Defining a mapping function

Supported bindings: ossia

It is common in audio to have a range which is best manipulated by the user in logarithmic space, for instance for adjusting frequencies, as the human hear is more sensitive to a 100Hz variation around 100Hz, than around 15kHz.

We can define custom mapping functions as part of the control, which will define how the UI controls shall behave:

struct {
  static consteval auto name() { return "foobar"; }

  struct mapper {
    static float map(float x) { return std::pow(x, 10.); }
    static float unmap(float y) { return std::pow(y, 1./10.); }
  };

  struct range {
    float min = 20.;
    float max = 20000.;
    float init = 100.;
  };

  float value{};
} foobar;

The mapping function must be a bijection of [0; 1] unto itself.

That is, the map and unmap function you choose must satisfy:

  • for all x in [0; 1] map(x) is in [0; 1]
  • for all x in [0; 1] unmap(x) is in [0; 1]
  • for all x in [0; 1] unmap(map(x)) == x
  • for all x in [0; 1] map(unmap(x)) == x

For instance, sin(x) or ln(x) do not work, but the x^N / x^(1/N) couple works.

Note that for now, mappings are only supported for floating-point sliders and knobs, in ossia.

With helpers

A few pre-made mappings are provided: the above could be rewritten as:

struct : halp::hslider_f32<"foobar", halp::range{20., 20000., 100.}> {
  using mapper = halp::pow_mapper<10>;
} foobar;

The complete list is provided in #include <halp/mappers.hpp>:

// #include: <halp/mappers.hpp>

#pragma once

#include <cmath>
#include <halp/modules.hpp>

#include <ratio>

HALP_MODULE_EXPORT
namespace halp
{

// FIXME: when clang supports double arguments we can use them here instead
template <typename Ratio>
struct log_mapper
{
  // Formula from http://benholmes.co.uk/posts/2017/11/logarithmic-potentiometer-laws
  static constexpr double mid = double(Ratio::num) / double(Ratio::den);
  static constexpr double b = (1. / mid - 1.) * (1. / mid - 1.);
  static constexpr double a = 1. / (b - 1.);
  static double map(double v) noexcept { return a * (std::pow(b, v) - 1.); }
  static double unmap(double v) noexcept { return std::log(v / a + 1.) / std::log(b); }
};

template <int Power>
struct pow_mapper
{
  static double map(double v) noexcept { return std::pow(v, (double)Power); }
  static double unmap(double v) noexcept { return std::pow(v, 1. / Power); }
};

template <typename T>
struct inverse_mapper
{
  static double map(double v) noexcept { return T::unmap(v); }
  static double unmap(double v) noexcept { return T::map(v); }
};

}