Frequency parameter / slider log scaling

I’m looking for logarithmic behaviour for a EQ frequency slider and it’s underlying parameter (I’d like automation to behave as expected).

I would like standard behaviour:
start: 20Hz
1/3point: 200Hz
midpoint: 632Hz
2/3point: 2000Hz
end: 20000Hz

But using skew of 0.199 in AudioParameterFloat gives:
start: 20Hz
1/3point: 99Hz
midpoint: 633Hz
2/3point: 2624Hz
end: 20000Hz

Can I override underlying behaviour to achieve my aim?

Maybe NormalisableRange<> ?

Many thanks in advance,
John.

1 Like

I suppose I’m looking for a different formula than NormalisableRange<>'s skewFactor offers. I’m looking for the top one, while skewFactor is the bottom… …x=Hz, y=0to1

How to go about adding/extending that capability to the NormalisableRange<> template?

1 Like

There’s a bunch of methods to let you use whatever mapping you want - see Slider::proportionOfLengthToValue etc

But, (correct me if I’m wrong) if I change the slider mapping the underlying parameter will still be incorrect? i.e. for host control and automation?

I was able to get the code to ‘work’ (for testing purposes) by altering juce_normalisablerange.h, raw parameter plays quite nicely in host and no need to touch slider side…

Somebody with better maths than me could generalise the solution to allow regular skew behaviour and more specific cases like this?

    /** Uses the properties of this mapping to convert a non-normalised value to
        its 0->1 representation.
    */
    ValueType convertTo0to1 (ValueType v) const noexcept
    {
        ValueType proportion = (v - start) / (end - start);

        if (skew != static_cast<ValueType> (1))
        {
            // proportion = std::pow (proportion, skew);            // Any skew value other than 1 will
                                                                    // give 20-20000 Hz log behaviour
            const ValueType twenty = static_cast<ValueType> (20);
            const ValueType three  = static_cast<ValueType> (3);

            proportion = (std::log10 (v / twenty)) / three;
        }

        return proportion;
    }

    /** Uses the properties of this mapping to convert a normalised 0->1 value to
        its full-range representation.
    */
    ValueType convertFrom0to1 (ValueType proportion) const noexcept
    {
        if (skew != static_cast<ValueType> (1) && proportion > ValueType())
        {
            //proportion = std::exp (std::log (proportion) / skew);
            const ValueType twenty = static_cast<ValueType> (20);
            const ValueType ten    = static_cast<ValueType> (10);
            const ValueType three  = static_cast<ValueType> (3);

            proportion = std::pow (ten, (three * proportion + std::log10 (twenty)));

            return proportion;
        }
        else
            return start + (end - start) * proportion;
    }

And/or somebody with better C++ than me could explain how I could use code like this with AudioParameterFloats without changing the JUCE file directly??

Many thanks in advance! John

How about this for juce_NormalisableRange.h ?

It should still preserve existing behaviour but adds true log behaviour…

/*
  ==============================================================================

   This file is part of the juce_core module of the JUCE library.
   Copyright (c) 2015 - ROLI Ltd.

   Permission to use, copy, modify, and/or distribute this software for any purpose with
   or without fee is hereby granted, provided that the above copyright notice and this
   permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
   TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
   NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
   DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
   IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
   CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

   ------------------------------------------------------------------------------

   NOTE! This permissive ISC license applies ONLY to files within the juce_core module!
   All other JUCE modules are covered by a dual GPL/commercial license, so if you are
   using any other modules, be sure to check that you also comply with their license.

   For more details, visit www.juce.com

  ==============================================================================
*/

#ifndef JUCE_NORMALISABLERANGE_H_INCLUDED
#define JUCE_NORMALISABLERANGE_H_INCLUDED


//==============================================================================
/**
    Represents a mapping between an arbitrary range of values and a
    normalised 0->1 range.

    The properties of the mapping also include an optional snapping interval
    and skew-factor.

    @see Range
*/
template <typename ValueType>
class NormalisableRange
{
public:
    /** Creates a continuous range that performs a dummy mapping. */
    NormalisableRange() noexcept  : start(), end (1), interval(), skew (static_cast<ValueType> (1)), skewLog (static_cast<ValueType> (0)) {}

    /** Creates a copy of another range. */
    NormalisableRange (const NormalisableRange& other) noexcept
        : start (other.start), end (other.end),
          interval (other.interval), skew (other.skew), skewLog (other.skewLog)
    {
        checkInvariants();
    }

    /** Creates a copy of another range. */
    NormalisableRange& operator= (const NormalisableRange& other) noexcept
    {
        start = other.start;
        end = other.end;
        interval = other.interval;
        skew = other.skew;
        skewLog = other.skewLog;
        checkInvariants();
        return *this;
    }

    /** Creates a NormalisableRange with a given range, interval and skew factor. */
    NormalisableRange (ValueType rangeStart,
                       ValueType rangeEnd,
                       ValueType intervalValue,
                       ValueType skewFactor,
                       ValueType skewLogFactor = 0) noexcept
        : start (rangeStart), end (rangeEnd),
          interval (intervalValue), skew (skewFactor), skewLog (skewLogFactor)
    {
        checkInvariants();
    }

    /** Creates a NormalisableRange with a given range and interval, but dummy skew-factors. */
    NormalisableRange (ValueType rangeStart,
                       ValueType rangeEnd,
                       ValueType intervalValue) noexcept
        : start (rangeStart), end (rangeEnd),
          interval (intervalValue), skew (static_cast<ValueType> (1)), skewLog (static_cast<ValueType> (0))
    {
        checkInvariants();
    }

    /** Creates a NormalisableRange with a given range, continuous interval, but a dummy skew-factor. */
    NormalisableRange (ValueType rangeStart,
                       ValueType rangeEnd) noexcept
        : start (rangeStart), end (rangeEnd),
          interval(), skew (static_cast<ValueType> (1)), skewLog (static_cast<ValueType> (0))
    {
        checkInvariants();
    }

    /** Uses the properties of this mapping to convert a non-normalised value to
        its 0->1 representation.
    */
    ValueType convertTo0to1 (ValueType v) const noexcept
    {
        ValueType proportion = (v - start) / (end - start);

        if (skew != static_cast<ValueType> (1) && skewLog == static_cast<ValueType>(0))
            proportion = std::pow (proportion, skew);

        if (skewLog != static_cast<ValueType> (0))
        {
            const ValueType one = static_cast<ValueType> (1.0);
            const ValueType ten = static_cast<ValueType> (10.0);
            const ValueType tenPowSkewLog = std::pow (ten, skewLog);
            proportion = (std::log10 ((proportion * (tenPowSkewLog - one)) + one)) / std::log10 (tenPowSkewLog);
        }

        return proportion;
    }

    /** Uses the properties of this mapping to convert a normalised 0->1 value to
        its full-range representation.
    */
    ValueType convertFrom0to1 (ValueType proportion) const noexcept
    {
        if (skew != static_cast<ValueType> (1) && proportion > ValueType() && skewLog == static_cast<ValueType>(0))
            proportion = std::exp (std::log (proportion) / skew);

        if (skewLog != static_cast<ValueType> (0))
        {
            const ValueType one = static_cast<ValueType> (1.0);
            const ValueType ten = static_cast<ValueType> (10.0);
            const ValueType tenPowSkewLog = std::pow (ten, skewLog);
            proportion = (std::pow (tenPowSkewLog, proportion) - one) / (tenPowSkewLog - one);
        }

        return start + (end - start) * proportion;
    }

    /** Takes a non-normalised value and snaps it based on the interval property of
        this NormalisedRange. */
    ValueType snapToLegalValue (ValueType v) const noexcept
    {
        if (interval > ValueType())
            v = start + interval * std::floor ((v - start) / interval + static_cast<ValueType> (0.5));

        if (v <= start || end <= start)
            return start;

        if (v >= end)
            return end;

        return v;
    }

    Range<ValueType> getRange() const noexcept          { return Range<ValueType> (start, end); }

    /** The start of the non-normalised range. */
    ValueType start;

    /** The end of the non-normalised range. */
    ValueType end;

    /** The snapping interval that should be used (in non-normalised value). Use 0 for a continuous range. */
    ValueType interval;

    /** An optional skew factor that alters the way values are distribute across the range.

        The skew factor lets you skew the mapping logarithmically so that larger or smaller
        values are given a larger proportion of the available space.

        A factor of 1.0 has no skewing effect at all. If the factor is < 1.0, the lower end
        of the range will fill more of the slider's length; if the factor is > 1.0, the upper
        end of the range will be expanded.
    */
    ValueType skew;

    /** If this skew factor is set the other is ignored. Uses a more computationally expensive
        calculation but is more accurate for things like decade based frequency scales.
        
        A factor of 1.0 will have 1 log decade per scale, 2.0 will give 2 etc. A factor of -1.0
        will be the inverse of +1.0. A factor of 0.0 will skip calculation allowing regular
        skew or linear behaviour.
        
        For a 3 decade Hz frequency scale, start and end should be 20 and 20000 respectively and
        skewLog should be set to 3.0.
    */
    ValueType skewLog;

private:
    void checkInvariants() const
    {
        jassert (end > start);
        jassert (interval >= ValueType());
        jassert (skew > ValueType());
    }
};


#endif   // JUCE_NORMALISABLERANGE_H_INCLUDED

Here’s a rough of the equations used in the conversion methods…

www.Desmos.com/calculator doesn’t support link sharing but here’s the new behaviour in green vs. current JUCE behaviour in blue…

1 Like

See my suggestion in here Suggestion: Replace skew with lambdas I believe this would also work as a solution for you as long as you are using C++11 or higher.

1 Like

Would love to see that. A good opportunity for me to practise lambdas too.

I’m trying to figure out how to write a lambda for lin to log conversion using the new and improved NormalisableRange, but I can’t wrap my head around it. I just don’t understand lambdas yet.

Old code:
addParameter (filterParam = new AudioParameterFloat("filter", "Filter Cutoff", NormalisableRange<float> (40.0f, 20000.0f, 0.0f, 0.3f, false), 20000.0f));

Does anybody have some example code? It’s for a filter cutoff AudioParameter, ranging from 40.0 tot 20000.0 Hz.

Stumbled upon this thread when helping out a friend and this is my solution:

 auto linToLogLambda = [](float min, float max, float linVal) { 
	float result = std::pow(10.0f, (std::log10(max/ min) * linVal + std::log10(min)));
	return result;
  };

And to get back to [0, 1]

  auto logToLinLambda = [](float min, float max, float logVal) { 
	float result =  (std::log10(logVal / min) / std::log10(max / min));
	return result;
};

when min=0 you will have a division by zero

Here is my proposal

struct LogarithmicRange
{
	template<class ValueType>
	static std::function<ValueType(ValueType currentRangeStart, ValueType currentRangeEnd, ValueType normalisedValue)> createConvertFrom0To1Func()
	{
	
		return std::function<ValueType(ValueType currentRangeStart, ValueType currentRangeEnd, ValueType normalisedValue)>(
			[]
			(ValueType minFrequency, ValueType maxFrequency, ValueType normalizedLog)
			{
				// always use doubles for internal calculation
				double minFrequencyLog = std::log(static_cast<double>(minFrequency));
				double maxFrequencyLog = std::log(static_cast<double>(maxFrequency));

			    return jlimit(static_cast<ValueType>(minFrequency), static_cast<ValueType>(maxFrequency),
					static_cast<ValueType>(
						  std::exp((static_cast<double>(normalizedLog)*(maxFrequencyLog - minFrequencyLog)) + minFrequencyLog)
						)
			   );
			}
			);
	}

	template<class ValueType>
	static std::function<ValueType(ValueType currentRangeStart, ValueType currentRangeEnd, ValueType mappedValue)> createConvertTo0To1Func()
	{
		return std::function<ValueType(ValueType currentRangeStart, ValueType currentRangeEnd, ValueType mappedValue)>([]
		(ValueType minFrequency, ValueType maxFrequency, ValueType frequency)
		{
			double minFrequencyLog = std::log(static_cast<double>(minFrequency));
			double maxFrequencyLog = std::log(static_cast<double>(maxFrequency));

			if (frequency <= minFrequency)
			{
				return static_cast<ValueType>(0);
			}
			else
				if (frequency >= maxFrequency)
				{
					return static_cast<ValueType>(1);
				}
				else
				{
					return static_cast<ValueType>((std::log(static_cast<double>(frequency)) - minFrequencyLog) / (maxFrequencyLog - minFrequencyLog));
				};
		});
	}

	template<class ValueType>
	static NormalisableRange<ValueType> createNormalisableRange(ValueType min, ValueType max)
	{
		return NormalisableRange<ValueType>(min, max,
			createConvertFrom0To1Func<ValueType>(),
			createConvertTo0To1Func<ValueType>(),
			nullptr);
	}
};

PS: You can use the created NormailsableRange direclty in the AudioParameterFloat broadcaster

Nice solution, but why all the boiler plate code? You create a struct which is essentially is a static function that returns a NormalisableRange with some predefined convertTo0to1 and convertFrom0to1 functions. Highly unnecessary.

I can’t remember why I did this, probably to have the convert routines individually available. Also it might be useful to calculate the log values in the factory function to save calculations. Feel free to improve it!