From cf790d239d4b5dfa772d5d1fdb112b8640a4a81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Nusser?= Date: Sat, 2 May 2020 12:21:16 +0200 Subject: Add powermap functionality to the engine for mapping input velocities through a Monotone cubic splines due to Steffen. --- plugin/Makefile.mingw32.in | 1 + src/Makefile.am | 2 + src/powermap.cc | 251 +++++++++++++++++++++++++++++++++++++++++++++ src/powermap.h | 76 ++++++++++++++ src/settings.h | 60 +++++++++++ test/Makefile.am | 15 ++- test/powermaptest.cc | 51 +++++++++ 7 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 src/powermap.cc create mode 100644 src/powermap.h create mode 100644 test/powermaptest.cc diff --git a/plugin/Makefile.mingw32.in b/plugin/Makefile.mingw32.in index 067a132..c577e02 100644 --- a/plugin/Makefile.mingw32.in +++ b/plugin/Makefile.mingw32.in @@ -38,6 +38,7 @@ DG_SRC = \ @top_srcdir@/src/midimapper.cc \ @top_srcdir@/src/path.cc \ @top_srcdir@/src/powerlist.cc \ + @top_srcdir@/src/powermap.cc \ @top_srcdir@/src/random.cc \ @top_srcdir@/src/sample.cc \ @top_srcdir@/src/sample_selection.cc \ diff --git a/src/Makefile.am b/src/Makefile.am index a8e5c74..09df750 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -58,6 +58,7 @@ libdg_la_SOURCES = \ midimapper.cc \ path.cc \ powerlist.cc \ + powermap.cc \ random.cc \ sample.cc \ sample_selection.cc \ @@ -113,6 +114,7 @@ EXTRA_DIST = \ path.h \ platform.h \ powerlist.h \ + powermap.h \ random.h \ range.h \ rangemap.h \ diff --git a/src/powermap.cc b/src/powermap.cc new file mode 100644 index 0000000..2bb45b7 --- /dev/null +++ b/src/powermap.cc @@ -0,0 +1,251 @@ +/* -*- Mode: c++ -*- */ +/*************************************************************************** + * powermap.cc + * + * Fri Apr 17 23:06:12 CEST 2020 + * Copyright 2020 André Nusser + * andre.nusser@googlemail.com + ****************************************************************************/ + +/* + * This file is part of DrumGizmo. + * + * DrumGizmo is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * DrumGizmo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with DrumGizmo; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +#include "powermap.h" + +#include +#include + +namespace +{ + +using Power = Powermap::Power; +using PowerPair = Powermap::PowerPair; + +Power h00(Power x) +{ + return (1 + 2 * x) * pow(1 - x, 2); +} + +Power h10(Power x) +{ + return x * pow(1 - x, 2); +} + +Power h01(Power x) +{ + return x * x * (3 - 2 * x); +} + +Power h11(Power x) +{ + return x * x * (x - 1); +} + +Power computeValue(const Power x, const PowerPair& P0, const PowerPair& P1, + const Power m0, const Power m1) +{ + const auto x0 = P0.in; + const auto x1 = P1.in; + const auto y0 = P0.out; + const auto y1 = P1.out; + const auto dx = x1 - x0; + const auto x_prime = (x - x0)/dx; + + return + h00(x_prime) * y0 + + h10(x_prime) * dx * m0 + + h01(x_prime) * y1 + + h11(x_prime) * dx * m1; +} + +} // end anonymous namespace + +Powermap::Powermap() +{ + reset(); +} + +Power Powermap::map(Power in) +{ + assert(in >= 0. && in <= 1.); + + if (spline_needs_update) + { + updateSpline(); + } + + Power out; + if (in < fixed[0].in) + { + out = shelf ? fixed[0].out + : computeValue(in, {0.,0.}, fixed[0], m[0], m[1]); + } + else if (in < fixed[1].in) + { + out = computeValue(in, fixed[0], fixed[1], m[1], m[2]); + } + else if (in < fixed[2].in) + { + out = computeValue(in, fixed[1], fixed[2], m[2], m[3]); + } + else + { + // in >= fixed[2].in + out = shelf ? fixed[2].out + : computeValue(in, fixed[2], {1.,1.}, m[3], m[4]); + } + + assert(out >= 0. && out <= 1.); + return out; +} + +void Powermap::reset() +{ + setFixed0({eps, eps}); + setFixed1({.5, .5}); + setFixed2({1 - eps, 1 - eps}); + // FIXME: better false? + shelf = true; + + updateSpline(); +} + +void Powermap::setFixed0(PowerPair new_value) +{ + if (fixed[0] != new_value) + { + spline_needs_update = true; + fixed[0].in = clamp(new_value.in, eps, fixed[1].in - eps); + fixed[0].out = clamp(new_value.out, eps, fixed[1].out - eps); + } +} + +void Powermap::setFixed1(PowerPair new_value) +{ + if (fixed[1] != new_value) + { + spline_needs_update = true; + fixed[1].in = clamp(new_value.in, fixed[0].in + eps, fixed[2].in - eps); + fixed[1].out = clamp(new_value.out, fixed[0].out + eps, fixed[2].out - eps); + } +} + +void Powermap::setFixed2(PowerPair new_value) +{ + if (fixed[2] != new_value) + { + spline_needs_update = true; + fixed[2].in = clamp(new_value.in, fixed[1].in + eps, 1 - eps); + fixed[2].out = clamp(new_value.out, fixed[1].out + eps, 1 - eps); + } +} + +void Powermap::setShelf(bool enable) +{ + if (shelf != enable) + { + spline_needs_update = true; + this->shelf = enable; + } +} + +PowerPair Powermap::getFixed0() const +{ + return fixed[0]; +} + +PowerPair Powermap::getFixed1() const +{ + return fixed[1]; +} + +PowerPair Powermap::getFixed2() const +{ + return fixed[2]; +} + +// This mostly followes the wikipedia article for monotone cubic splines: +// https://en.wikipedia.org/wiki/Monotone_cubic_interpolation +void Powermap::updateSpline() +{ + assert(0. <= fixed[0].in && fixed[0].in < fixed[1].in && + fixed[1].in < fixed[2].in && fixed[2].in <= 1.); + assert(0. <= fixed[0].out && fixed[0].out <= fixed[1].out && + fixed[1].out <= fixed[2].out && fixed[2].out <= 1.); + + Powers X = shelf ? Powers{fixed[0].in, fixed[1].in, fixed[2].in} + : Powers{0., fixed[0].in, fixed[1].in, fixed[2].in, 1.}; + Powers Y = shelf ? Powers{fixed[0].out, fixed[1].out, fixed[2].out} + : Powers{0., fixed[0].out, fixed[1].out, fixed[2].out, 1.}; + + auto slopes = calcSlopes(X, Y); + + if (shelf) + { + assert(slopes.size() == 3); + this->m[1] = slopes[0]; + this->m[2] = slopes[1]; + this->m[3] = slopes[2]; + } + else + { + assert(slopes.size() == 5); + for (std::size_t i = 0; i < m.size(); ++i) + { + this->m[i] = slopes[i]; + } + } + + spline_needs_update = false; +} + +// This follows the monotone cubic spline algorithm of Steffen, from: +// "A Simple Method for Monotonic Interpolation in One Dimension" +std::vector Powermap::calcSlopes(const Powers& X, const Powers& Y) +{ + Powers m(X.size()); + + Powers d(X.size() - 1); + Powers h(X.size() - 1); + for (std::size_t i = 0; i < d.size(); ++i) + { + h[i] = X[i + 1] - X[i]; + d[i] = (Y[i + 1] - Y[i]) / h[i]; + } + + m.front() = d.front(); + for (std::size_t i = 1; i < m.size() - 1; ++i) + { + m[i] = (d[i - 1] + d[i]) / 2.; + } + m.back() = d.back(); + + for (std::size_t i = 1; i < m.size() - 1; ++i) + { + const auto min_d = 2*std::min(d[i - 1], d[i]); + m[i] = + std::min(min_d, + (h[i] * d[i - 1] + h[i - 1] * d[i]) / (h[i - 1] + h[i])); + } + + return m; +} + +Power Powermap::clamp(Power in, Power min, Power max) const +{ + return std::max(min, std::min(in, max)); +} diff --git a/src/powermap.h b/src/powermap.h new file mode 100644 index 0000000..3a406cc --- /dev/null +++ b/src/powermap.h @@ -0,0 +1,76 @@ +/* -*- Mode: c++ -*- */ +/*************************************************************************** + * powermap.h + * + * Fri Apr 17 23:06:12 CEST 2020 + * Copyright 2020 André Nusser + * andre.nusser@googlemail.com + ****************************************************************************/ + +/* + * This file is part of DrumGizmo. + * + * DrumGizmo is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * DrumGizmo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with DrumGizmo; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +#pragma once + +#include +#include + +class Powermap +{ +public: + using Power = float; + using Powers = std::vector; + struct PowerPair + { + Power in; + Power out; + + bool operator!=(const PowerPair& other) + { + return in != other.in || out != other.out; + } + }; + + Powermap(); + + Power map(Power in); + void reset(); + + void setFixed0(PowerPair new_value); + void setFixed1(PowerPair new_value); + void setFixed2(PowerPair new_value); + void setShelf(bool enable); + + PowerPair getFixed0() const; + PowerPair getFixed1() const; + PowerPair getFixed2() const; + +private: + // input parameters (state of this class) + std::array fixed; + bool shelf; + + // spline parameters (deterministically computed from the input parameters) + bool spline_needs_update; + std::array m; + const Power eps = 1e-4; + + void updateSpline(); + std::vector calcSlopes(const Powers& X, const Powers& P); + + Power clamp(Power in, Power min, Power max) const; +}; diff --git a/src/settings.h b/src/settings.h index 9fa3896..a3d21d0 100644 --- a/src/settings.h +++ b/src/settings.h @@ -144,6 +144,20 @@ struct Settings // Current latency offset in ms - for UI Atomic latency_current{0}; + // Powermap parameters + Atomic enable_powermap; + Atomic powermap_fixed0_x{0.}; + Atomic powermap_fixed0_y{0.}; + Atomic powermap_fixed1_x{.5}; + Atomic powermap_fixed1_y{.5}; + Atomic powermap_fixed2_x{1.}; + Atomic powermap_fixed2_y{1.}; + Atomic powermap_shelf{true}; + + // Powermap visualizer; -1 is "none" + Atomic powermap_input{-1.}; + Atomic powermap_output{-1.}; + Atomic audition_counter{0}; Atomic audition_instrument; Atomic audition_velocity; @@ -209,6 +223,18 @@ struct SettingsGetter SettingRef latency_regain; SettingRef latency_current; + SettingRef enable_powermap; + SettingRef powermap_fixed0_x; + SettingRef powermap_fixed0_y; + SettingRef powermap_fixed1_x; + SettingRef powermap_fixed1_y; + SettingRef powermap_fixed2_x; + SettingRef powermap_fixed2_y; + SettingRef powermap_shelf; + + SettingRef powermap_input; + SettingRef powermap_output; + SettingRef audition_counter; SettingRef audition_instrument; SettingRef audition_velocity; @@ -257,6 +283,16 @@ struct SettingsGetter , latency_stddev{settings.latency_stddev} , latency_regain{settings.latency_regain} , latency_current{settings.latency_current} + , enable_powermap{settings.enable_powermap} + , powermap_fixed0_x{settings.powermap_fixed0_x} + , powermap_fixed0_y{settings.powermap_fixed0_y} + , powermap_fixed1_x{settings.powermap_fixed1_x} + , powermap_fixed1_y{settings.powermap_fixed1_y} + , powermap_fixed2_x{settings.powermap_fixed2_x} + , powermap_fixed2_y{settings.powermap_fixed2_y} + , powermap_shelf{settings.powermap_shelf} + , powermap_input{settings.powermap_input} + , powermap_output{settings.powermap_output} , audition_counter{settings.audition_counter} , audition_instrument{settings.audition_instrument} , audition_velocity{settings.audition_velocity} @@ -321,6 +357,18 @@ public: Notifier latency_regain; Notifier latency_current; + Notifier enable_powermap; + Notifier powermap_fixed0_x; + Notifier powermap_fixed0_y; + Notifier powermap_fixed1_x; + Notifier powermap_fixed1_y; + Notifier powermap_fixed2_x; + Notifier powermap_fixed2_y; + Notifier powermap_shelf; + + Notifier powermap_input; + Notifier powermap_output; + Notifier audition_counter; Notifier audition_instrument; Notifier audition_velocity; @@ -383,6 +431,18 @@ public: EVAL(latency_regain); EVAL(latency_current); + EVAL(enable_powermap); + EVAL(powermap_fixed0_x); + EVAL(powermap_fixed0_y); + EVAL(powermap_fixed1_x); + EVAL(powermap_fixed1_y); + EVAL(powermap_fixed2_x); + EVAL(powermap_fixed2_y); + EVAL(powermap_shelf); + + EVAL(powermap_input); + EVAL(powermap_output); + EVAL(audition_counter); EVAL(audition_instrument); EVAL(audition_velocity); diff --git a/test/Makefile.am b/test/Makefile.am index 001d13d..c9ece6e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -8,7 +8,7 @@ TESTS = resource enginetest paintertest configfile audiocache \ randomtest atomictest syncedsettingstest imagecachetest \ semaphoretest drumkitcreatortest bytesizeparsertest notifiertest \ dgxmlparsertest domloadertest configparsertest midimapparsertest \ - eventsdstest + eventsdstest powermaptest EXTRA_DIST = \ dgunit.h \ @@ -109,7 +109,7 @@ paintertest_CXXFLAGS = -DOUTPUT=\"paintertest\" \ $(DEBUG_FLAGS) \ -I$(top_srcdir)/src -I$(top_srcdir)/plugingui \ -I$(top_srcdir)/hugin $(PTHREAD_CFLAGS) -paintertest_LDFLAGS = $(top_srcdir)/plugingui/libdggui.la +paintertest_LDFLAGS = $(top_builddir)/plugingui/libdggui.la paintertest_SOURCES = \ $(top_srcdir)/hugin/hugin.c \ dgtest.cc \ @@ -167,7 +167,7 @@ syncedsettingstest_SOURCES = syncedsettings.cc dgtest.cc imagecachetest_CXXFLAGS = -DOUTPUT=\"imagecachetest\" \ $(DEBUG_FLAGS) \ -I$(top_srcdir)/src -I$(top_srcdir)/plugingui -I$(top_srcdir)/hugin -imagecachetest_LDFLAGS = $(top_srcdir)/plugingui/libdggui.la +imagecachetest_LDFLAGS = $(top_builddir)/plugingui/libdggui.la imagecachetest_SOURCES = \ $(top_srcdir)/hugin/hugin.c \ imagecachetest.cc \ @@ -270,4 +270,13 @@ eventsdstest_SOURCES = \ eventsdstest.cc \ dgtest.cc +powermaptest_CXXFLAGS = -DOUTPUT=\"powermaptest\" \ + $(DEBUG_FLAGS) \ + -I$(top_srcdir)/src +powermaptest_LDFLAGS = +powermaptest_SOURCES = \ + $(top_srcdir)/src/powermap.cc \ + powermaptest.cc \ + dgtest.cc + endif diff --git a/test/powermaptest.cc b/test/powermaptest.cc new file mode 100644 index 0000000..3e94575 --- /dev/null +++ b/test/powermaptest.cc @@ -0,0 +1,51 @@ +/* -*- Mode: c++ -*- */ +/*************************************************************************** + * powermaptest.cc + * + * Sun Apr 19 23:23:37 CEST 2020 + * Copyright 2020 André Nusser + * andre.nusser@googlemail.com + ****************************************************************************/ + +/* + * This file is part of DrumGizmo. + * + * DrumGizmo is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * DrumGizmo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with DrumGizmo; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +#include "dgunit.h" + +#include "../src/powermap.h" + +class test_powermaptest + : public DGUnit +{ +public: + test_powermaptest() + { + DGUNIT_TEST(test_powermaptest::check_values); + } + + void check_values() + { + Powermap powermap; + + // TODO + // std::cout << powermap.map(.8) << std::endl; + // DGUNIT_ASSERT_EQUAL(powermap.map(.8), .8); + } +}; + +// Registers the fixture into the 'registry' +static test_powermaptest test; -- cgit v1.2.3