From 357b17d55f49793a4b24eccce1da6b7153ab4fff Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 3 Mar 2020 13:11:31 +0100 Subject: [PATCH 01/21] Add curves --- cmake/SfizzConfig.cmake | 4 + src/CMakeLists.txt | 5 +- src/external/spline/LICENSE | 29 +++ src/external/spline/spline/spline.cpp | 77 ++++++++ src/external/spline/spline/spline.h | 46 +++++ src/sfizz/Curve.cpp | 249 ++++++++++++++++++++++++++ src/sfizz/Curve.h | 125 +++++++++++++ src/sfizz/Curve.hpp | 33 ++++ tests/CMakeLists.txt | 2 + tests/PlotCurve.cpp | 94 ++++++++++ 10 files changed, 662 insertions(+), 2 deletions(-) create mode 100644 src/external/spline/LICENSE create mode 100644 src/external/spline/spline/spline.cpp create mode 100644 src/external/spline/spline/spline.h create mode 100644 src/sfizz/Curve.cpp create mode 100644 src/sfizz/Curve.h create mode 100644 src/sfizz/Curve.hpp create mode 100644 tests/PlotCurve.cpp diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 0f5ebcf1..68c02078 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -50,6 +50,7 @@ else() endif() endif() + # If we build with Clang use libc++ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT ANDROID) set(USE_LIBCPP ON CACHE BOOL "Use libc++ with clang") @@ -64,6 +65,9 @@ endif() add_library(sfizz-pugixml STATIC "src/external/pugixml/src/pugixml.cpp") target_include_directories(sfizz-pugixml PUBLIC "src/external/pugixml/src") +add_library(sfizz-spline STATIC "src/external/spline/spline/spline.cpp") +target_include_directories(sfizz-spline PUBLIC "src/external/spline") + include (CheckLibraryExists) if (UNIX AND NOT APPLE) check_library_exists(atomic __atomic_load "" LIBATOMIC_FOUND) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f1378612..7b37e907 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ set (SFIZZ_SOURCES sfizz/FloatEnvelopes.cpp sfizz/Logger.cpp sfizz/SfzFilter.cpp + sfizz/Curve.cpp sfizz/Effects.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp @@ -36,7 +37,7 @@ target_sources(sfizz_static PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp sfi target_include_directories (sfizz_static PUBLIC .) target_include_directories (sfizz_static PUBLIC external) target_link_libraries (sfizz_static PUBLIC absl::strings absl::span) -target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml) +target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline) set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") if (WIN32) target_compile_definitions (sfizz_static PRIVATE _USE_MATH_DEFINES) @@ -69,7 +70,7 @@ if (SFIZZ_SHARED) target_sources(sfizz_shared PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp sfizz/sfizz.cpp) target_include_directories (sfizz_shared PRIVATE .) target_include_directories (sfizz_shared PRIVATE external) - target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml) + target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline) if (WIN32) target_compile_definitions (sfizz_shared PRIVATE _USE_MATH_DEFINES) endif() diff --git a/src/external/spline/LICENSE b/src/external/spline/LICENSE new file mode 100644 index 00000000..d6ece988 --- /dev/null +++ b/src/external/spline/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Roland Rabien +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/external/spline/spline/spline.cpp b/src/external/spline/spline/spline.cpp new file mode 100644 index 00000000..1db58145 --- /dev/null +++ b/src/external/spline/spline/spline.cpp @@ -0,0 +1,77 @@ +/*============================================================================== + + Copyright 2018 by Roland Rabien, Devin Lane + For more information visit www.rabiensoftware.com + + ==============================================================================*/ + +/* "THE BEER-WARE LICENSE" (Revision 42): Devin Lane wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, and you + * think this stuff is worth it, you can buy me a beer in return. */ + +#include "spline.h" +#include + +Spline::Spline (const double *pointsX, const double *pointsY, int numPoints) +{ + assert (numPoints >= 3); // "Must have at least three points for interpolation" + + int n = numPoints - 1; + + std::vector b, d, a, c, l, u, z, h; + + a.resize (n); + b.resize (n); + c.resize (n + 1); + d.resize (n); + h.resize (n + 1); + l.resize (n + 1); + u.resize (n + 1); + z.resize (n + 1); + + l[0] = 1.0; + u[0] = 0.0; + z[0] = 0.0; + h[0] = pointsX[1] - pointsX[0]; + + for (int i = 1; i < n; i++) + { + h[i] = pointsX[i+1] - pointsX[i]; + l[i] = (2 * (pointsX[i+1] - pointsX[i-1])) - (h[i-1]) * u[i-1]; + u[i] = (h[i]) / l[i]; + a[i] = (3.0 / (h[i])) * (pointsY[i+1] - pointsY[i]) - (3.0 / (h[i-1])) * (pointsY[i] - pointsY[i-1]); + z[i] = (a[i] - (h[i-1]) * z[i-1]) / l[i]; + } + + l[n] = 1.0; + z[n] = 0.0; + c[n] = 0.0; + + for (int j = n - 1; j >= 0; j--) + { + c[j] = z[j] - u[j] * c[j+1]; + b[j] = (pointsY[j+1] - pointsY[j]) / (h[j]) - ((h[j]) * (c[j+1] + 2.0 * c[j])) / 3.0; + d[j] = (c[j+1] - c[j]) / (3.0 * h[j]); + } + + elements.reserve (n); + for (int i = 0; i < n; i++) + elements.emplace_back (pointsX[i], pointsY[i], b[i], c[i], d[i]); +} + +double Spline::interpolate (double x) const noexcept +{ + int n = static_cast(elements.size()); + if (n == 0) + return 0.0; + + int i; + for (i = 0; i < n; i++) + { + if (! (elements[i] < x)) + break; + } + if (i != 0) i--; + + return elements[i].eval (x); +} diff --git a/src/external/spline/spline/spline.h b/src/external/spline/spline/spline.h new file mode 100644 index 00000000..1d115521 --- /dev/null +++ b/src/external/spline/spline/spline.h @@ -0,0 +1,46 @@ +/*============================================================================== + + Copyright 2018 by Roland Rabien, Devin Lane + For more information visit www.rabiensoftware.com + + ==============================================================================*/ + +#pragma once +#include + +/** Cubic spline interpolation is a simple way to obtain a smooth curve from a set of + discrete points. It has both C1 (first derivative) and C2 (second derivative) continuity, + enabling it to produce a continuous piecewise function given a set of data points. + + Add points in increasing x order */ +class Spline +{ +public: + Spline (const double *pointsX, const double *pointsY, int numPoints); + + double operator[] (double x) const noexcept { return interpolate(x); } + double interpolate (double x) const noexcept; + + class Element + { + public: + Element (double x_ = 0) : x (x_) {} + + Element (double x_, double a_, double b_, double c_, double d_) + : x (x_), a (a_), b (b_), c (c_), d (d_) {} + + double eval (double xx) const + { + double xix (xx - x); + return a + b * xix + c * (xix * xix) + d * (xix * xix * xix); + } + + bool operator< (const Element& e) const { return x < e.x; } + bool operator< (const double xx) const { return x < xx; } + + double x = 0, a = 0, b = 0, c = 0, d = 0; + }; + +private: + std::vector elements; +}; diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp new file mode 100644 index 00000000..01f121a6 --- /dev/null +++ b/src/sfizz/Curve.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Curve.h" +#include "Opcode.h" +#include "Debug.h" +#include +#include + +namespace sfz +{ + +static const Curve defaultCurve = Curve::buildBipolar(0.0, 1.0); + +Curve Curve::buildCurveFromHeader( + absl::Span members, Interpolator itp, bool limit) +{ + Curve curve; + bool fillStatus[NumValues] = {}; + const Range fullRange { -HUGE_VALF, +HUGE_VALF }; + + auto setPoint = [&curve, &fillStatus](int i, float x) { + curve._points[i] = x; + fillStatus[i] = true; + }; + + // fill curve ends with default values (verified) + setPoint(0, 0.0); + setPoint(NumValues - 1, 1.0); + + for (const Opcode& opc : members) { + unsigned index; + + if (opc.lettersOnlyHash != hash("v")) + continue; + + auto indexOpt = opc.backParameter(); + if (!indexOpt || (index = *indexOpt) >= NumValues) + continue; + + auto valueOpt = readOpcode(opc.value, fullRange); + if (!valueOpt) + continue; + + setPoint(index, *valueOpt); + } + + curve.fill(itp, fillStatus); + + if (limit) { + for (unsigned i = 0; i < NumValues; ++i) + curve._points[i] = clamp(curve._points[i], -1.0f, +1.0f); + } + + return curve; +} + +Curve Curve::buildPredefinedCurve(int index) +{ + Curve curve; + + switch (index) { + default: + ASSERTFALSE; + case 0: + curve = buildBipolar(0, 1); + break; + case 1: + curve = buildBipolar(-1, +1); + break; + case 2: + curve = buildBipolar(1, 0); + break; + case 3: + curve = buildBipolar(+1, -1); + break; + case 4: + for (unsigned i = 0; i < NumValues; ++i) { + double x = i * (1.0 / (NumValues - 1)); + curve._points[i] = x * x; + } + break; + case 5: + for (unsigned i = 0; i < NumValues; ++i) { + double x = i * (1.0 / (NumValues - 1)); + curve._points[i] = std::pow(x, 0.5); + } + break; + case 6: + for (unsigned i = 0; i < NumValues; ++i) { + double x = i * (1.0 / (NumValues - 1)); + curve._points[i] = std::pow(1.0 - x, 0.5); + } + break; + } + + return curve; +} + +Curve Curve::buildBipolar(float v1, float v2) +{ + Curve curve; + bool fillStatus[NumValues] = {}; + + curve._points[0] = v1; + curve._points[NumValues - 1] = v2; + + fillStatus[0] = true; + fillStatus[NumValues - 1] = true; + + curve.lerpFill(fillStatus); + return curve; +} + +const Curve& Curve::getDefault() +{ + return defaultCurve; +} + +void Curve::fill(Interpolator itp, const bool fillStatus[NumValues]) +{ + switch (itp) { + default: + case Interpolator::Linear: + lerpFill(fillStatus); + break; + case Interpolator::Spline: + splineFill(fillStatus); + break; + } +} + +void Curve::lerpFill(const bool fillStatus[NumValues]) +{ + for (int iCurr = 1; iCurr < NumValues - 1; ++iCurr) { + int iLeft, iRight; + iLeft = iCurr - 1; + for (iRight = iCurr + 1; iRight < 127 && !fillStatus[iRight]; ++iRight); + + float mu = static_cast(iCurr - iLeft) / (iRight - iLeft); + _points[iCurr] = _points[iLeft] + mu * (_points[iRight] - _points[iLeft]); + } +} + +void Curve::splineFill(const bool fillStatus[NumValues]) +{ + std::array x; + std::array y; + unsigned count = 0; + + for (unsigned i = 0; i < NumValues; ++i) { + if (fillStatus[i]) { + x[count] = i; + y[count] = _points[i]; + ++count; + } + } + + Spline spline(x.data(), y.data(), count); + for (unsigned i = 0; i < NumValues; ++i) { + if (!fillStatus[i]) + _points[i] = spline.interpolate(i); + } +} + +/// +CurveSet CurveSet::createPredefined() +{ + CurveSet cs; + cs._curves.reserve(16); + + for (unsigned i = 0; i < Curve::NumPredefinedCurves; ++i) { + Curve curve = Curve::buildPredefinedCurve(i); + cs._curves.emplace_back(new Curve(curve)); + } + + return cs; +} + +void CurveSet::addCurve(const Curve& curve, int explicitIndex) +{ + std::unique_ptr* slot; + + if (explicitIndex == -1) { + if (_useExplicitIndexing) + return; // reject implicit indices if any were explicit before + _curves.emplace_back(); + slot = &_curves.back(); + } else { + size_t index = static_cast(explicitIndex); + if (index >= _curves.size()) + _curves.resize(index + 1); + _useExplicitIndexing = true; + slot = &_curves[index]; + } + + slot->reset(new Curve(curve)); +} + +void CurveSet::addCurveFromHeader(absl::Span members) +{ + auto findOpcode = [members](uint64_t name_hash) -> const Opcode* { + const Opcode* opc = nullptr; + for (size_t i = members.size(); !opc && i-- > 0;) { + if (members[i].lettersOnlyHash == name_hash) + opc = &members[i]; + } + return opc; + }; + + int curveIndex = -1; + Curve::Interpolator itp = Curve::Interpolator::Linear; + + if (const Opcode* opc = findOpcode(hash("curve_index"))) { + if (auto opt = readOpcode(opc->value, {0, 255})) + curveIndex = *opt; + else + DBG("Invalid value for curve index: " << opc.value); + } + +#if 0 // potential sfizz extension + if (const Opcode* opc = findOpcode(hash("sfizz:curve_interpolator"))) { + if (opc->value == "spline") + itp = Curve::Interpolator::Spline; + else if (opc->value != "linear") + DBG("Invalid value for curve interpolator: " << opc.value); + } +#endif + + addCurve(Curve::buildCurveFromHeader(members, itp), curveIndex); +} + +const Curve& CurveSet::getCurve(unsigned index) const +{ + const Curve* curve = nullptr; + + if (index < _curves.size()) + curve = _curves[index].get(); + + if (!curve) + curve = &Curve::getDefault(); + + return *curve; +} + +} // namespace sfz diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h new file mode 100644 index 00000000..3180309b --- /dev/null +++ b/src/sfizz/Curve.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "absl/types/span.h" +#include +#include +#include + +namespace sfz +{ +struct Opcode; + +/** + * @brief A value-mapping controller curve, built-in or user-defined. + */ +class Curve { +public: + /** + * @brief Compute the curve for integral x in domain [0:127] + */ + float evalCC7(int value7) const; + + /** + * @brief Compute the curve for real x in domain [0:127] + */ + float evalCC7(float value7) const; + + /** + * @brief Compute the curve for real x in domain [0:1] + */ + float evalNormalized(float value) const; + + /** + * @brief Kind of curve interpolator + */ + enum class Interpolator { + Linear, + Spline, + }; + + /** + * @brief Build a curve based on contents of a header + * + * @param members contents of the header + * @param itp kind of interpolator to fill between values + * @param limit whether to force values in domain [-1:+1] + */ + static Curve buildCurveFromHeader( + absl::Span members, + Interpolator itp = Interpolator::Linear, bool limit = false); + + /** + * @brief Number of predefined curves + */ + enum { NumPredefinedCurves = 7 }; + + /** + * @brief Build a predefined curve + */ + static Curve buildPredefinedCurve(int index); + + /** + * @brief Build a linear curve from v1 to v2 + */ + static Curve buildBipolar(float v1, float v2); + + /** + * @brief Get a linear curve from 0 to 1 + */ + static const Curve& getDefault(); + +private: + enum { NumValues = 128 }; + +private: + void fill(Interpolator itp, const bool fillStatus[NumValues]); + void lerpFill(const bool fillStatus[NumValues]); + void splineFill(const bool fillStatus[NumValues]); + +private: + std::array _points { }; +}; + +/** + * @brief A collection of curves organized by index. + */ +class CurveSet { +public: + /* + * @brief Create a curve set initialized with the default curves. + */ + static CurveSet createPredefined(); + + /* + * @brief Add a curve. + * + * @param curve a curve to add + * @param explicitIndex if not -1, the explicit index of the curve + */ + void addCurve(const Curve& curve, int explicitIndex = -1); + + /** + * @brief Add a curve based on contents of a header + * + * @param members contents of the header + */ + void addCurveFromHeader(absl::Span members); + + /** + * @brief Get a curve given its index + */ + const Curve& getCurve(unsigned index) const; + +private: + std::vector> _curves; + bool _useExplicitIndexing = false; +}; + +} // namespace sfz + +#include "Curve.hpp" diff --git a/src/sfizz/Curve.hpp b/src/sfizz/Curve.hpp new file mode 100644 index 00000000..0881ae45 --- /dev/null +++ b/src/sfizz/Curve.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Curve.h" +#include "MathHelpers.h" + +namespace sfz +{ + +inline float Curve::evalCC7(int value7) const +{ + return _points[clamp(value7, 0, 127)]; +} + +inline float Curve::evalCC7(float value7) const +{ + value7 = clamp(value7, 0.0f, 127.0f); + int i1 = static_cast(value7); + int i2 = std::min(127, i1 + 1); + float mu = value7 - i1; + return _points[i1] + mu * (_points[i2] - _points[i1]); +} + +inline float Curve::evalNormalized(float value) const +{ + return evalCC7(127 * value); +} + +} // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 454f718f..7ed0d32a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,5 +56,7 @@ target_link_libraries(eq_apply PRIVATE sfizz::sfizz) add_executable(filter_apply Filter.cpp) target_link_libraries(filter_apply PRIVATE sfizz::sfizz) +add_executable(sfizz_plot_curve PlotCurve.cpp) +target_link_libraries(sfizz_plot_curve PRIVATE sfizz::sfizz) file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests) diff --git a/tests/PlotCurve.cpp b/tests/PlotCurve.cpp new file mode 100644 index 00000000..a69d4593 --- /dev/null +++ b/tests/PlotCurve.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/** + This program generates the data file of a LFO output recorded for a fixed + duration. The file contains columns for each LFO in the SFZ region. + The columns are: Time, Lfo1, ... LfoN + One can use Gnuplot to display this data. + Example: + sfizz_plot_lfo file.sfz > lfo.dat + gnuplot + plot "lfo.dat" using 1:2 with lines + */ + +#include "sfizz/Curve.h" +#include "sfizz/Parser.h" +#include "absl/strings/numbers.h" +#include "absl/types/span.h" +#include +#include + +//============================================================================== + +/** + * @brief Print usage information + */ +static void usage() +{ + std::cerr << "Usage: sfizz_plot_curve [file.sfz]" + << "\n"; +} + +/** + * @brief Parser which extracts the configuration of curves + */ +class CurveParser : public sfz::Parser { +public: + explicit CurveParser(sfz::CurveSet& curveSet) + : curveSet(curveSet) + { + } + + void callback(absl::string_view header, const std::vector& members) override + { + if (header == "effect") + curveSet.addCurveFromHeader(members); + } + +private: + sfz::CurveSet& curveSet; +}; + +/** + * @brief Program which loads LFO configuration and generates plot data for the given duration. + */ +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) + return usage(), 1; + + int curveIndex = -1; + if (!absl::SimpleAtoi(argv[1], &curveIndex)) + return usage(), 1; + + std::string filePath; + if (argc > 2) + filePath = argv[2]; + + /// + sfz::CurveSet curveSet = sfz::CurveSet::createPredefined(); + + if (!filePath.empty()) { + CurveParser parser(curveSet); + if (!parser.loadSfzFile(filePath)) { + std::cerr << "Cannot load SFZ: " << filePath << "\n"; + return 1; + } + } + + /// + const sfz::Curve& curve = curveSet.getCurve(curveIndex); + + constexpr unsigned numSamples = 1024; // number of points to evaluate + + for (unsigned i = 0; i < numSamples; ++i) { + float x = i * (1.0f / (numSamples - 1)); + std::cout << x << ' ' << curve.evalNormalized(x) << '\n'; + } + + return 0; +} From a7b21a989096f4dd790d38a4381c0d68a74eba4c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 3 Mar 2020 15:52:49 +0100 Subject: [PATCH 02/21] Fallback interpolator to lerp if not enough points --- src/sfizz/Curve.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 01f121a6..0c077b1a 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -159,6 +159,9 @@ void Curve::splineFill(const bool fillStatus[NumValues]) } } + if (count < 3) + return lerpFill(fillStatus); + Spline spline(x.data(), y.data(), count); for (unsigned i = 0; i < NumValues; ++i) { if (!fillStatus[i]) From e1bacb28880f14de0b2272bf5156d1b33b0be008 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 3 Mar 2020 16:04:28 +0100 Subject: [PATCH 03/21] Curves integration into the synth --- src/sfizz/Curve.h | 5 +++++ src/sfizz/Curve.hpp | 6 ++++++ src/sfizz/Synth.cpp | 7 +++---- src/sfizz/Synth.h | 5 ++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h index 3180309b..dff2f86e 100644 --- a/src/sfizz/Curve.h +++ b/src/sfizz/Curve.h @@ -115,6 +115,11 @@ public: */ const Curve& getCurve(unsigned index) const; + /** + * @brief Get the number of slots. + */ + unsigned getNumCurves() const; + private: std::vector> _curves; bool _useExplicitIndexing = false; diff --git a/src/sfizz/Curve.hpp b/src/sfizz/Curve.hpp index 0881ae45..1ebead18 100644 --- a/src/sfizz/Curve.hpp +++ b/src/sfizz/Curve.hpp @@ -30,4 +30,10 @@ inline float Curve::evalNormalized(float value) const return evalCC7(127 * value); } +/// +inline unsigned CurveSet::getNumCurves() const +{ + return static_cast(_curves.size()); +} + } // namespace sfz diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b02ebd00..816d0b26 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -71,8 +71,7 @@ void sfz::Synth::callback(absl::string_view header, const std::vector& m buildRegion(members); break; case hash("curve"): - // TODO: implement curves - numCurves++; + curves.addCurveFromHeader(members); break; case hash("effect"): handleEffectOpcodes(members); @@ -128,11 +127,11 @@ void sfz::Synth::clear() effectBuses[0]->setGainToMain(1.0); effectBuses[0]->setSamplesPerBlock(samplesPerBlock); effectBuses[0]->setSampleRate(sampleRate); + curves = CurveSet::createPredefined(); resources.filePool.clear(); resources.logger.clear(); numGroups = 0; numMasters = 0; - numCurves = 0; fileTicket = -1; defaultSwitch = absl::nullopt; defaultPath = ""; @@ -717,7 +716,7 @@ int sfz::Synth::getNumMasters() const noexcept } int sfz::Synth::getNumCurves() const noexcept { - return numCurves; + return curves.getNumCurves(); } std::string sfz::Synth::exportMidnam(absl::string_view model) const diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 616cf60d..41a56c53 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -10,6 +10,7 @@ #include "Voice.h" #include "Region.h" #include "Effects.h" +#include "Curve.h" #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" @@ -382,7 +383,6 @@ private: int numGroups { 0 }; int numMasters { 0 }; - int numCurves { 0 }; /** * @brief Remove all regions, resets all voices and clears everything @@ -461,6 +461,9 @@ private: typedef std::unique_ptr EffectBusPtr; std::vector effectBuses; // 0 is "main", 1-N are "fx1"-"fxN" + // Curves + CurveSet curves; + // Intermediate buffers AudioBuffer tempBuffer { 2, config::defaultSamplesPerBlock }; AudioBuffer tempMixNodeBuffer { 2, config::defaultSamplesPerBlock }; From bfc3f53447125faf9729e2025bede3d40a7f3537 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 3 Mar 2020 16:12:31 +0100 Subject: [PATCH 04/21] Fix the bad header name in the curve test --- tests/PlotCurve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PlotCurve.cpp b/tests/PlotCurve.cpp index a69d4593..5ca42c28 100644 --- a/tests/PlotCurve.cpp +++ b/tests/PlotCurve.cpp @@ -45,7 +45,7 @@ public: void callback(absl::string_view header, const std::vector& members) override { - if (header == "effect") + if (header == "curve") curveSet.addCurveFromHeader(members); } From 784d1a39712e094ce2017470156645bb84809089 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 7 Mar 2020 11:48:03 +0100 Subject: [PATCH 05/21] Adapt to the parser rework --- src/sfizz/Curve.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 0c077b1a..355d660b 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -32,13 +32,11 @@ Curve Curve::buildCurveFromHeader( setPoint(NumValues - 1, 1.0); for (const Opcode& opc : members) { - unsigned index; - - if (opc.lettersOnlyHash != hash("v")) + if (opc.lettersOnlyHash != hash("v&")) continue; - auto indexOpt = opc.backParameter(); - if (!indexOpt || (index = *indexOpt) >= NumValues) + unsigned index = opc.parameters.back(); + if (index >= NumValues) continue; auto valueOpt = readOpcode(opc.value, fullRange); From c572bebb188ce0b2e8f1848b6c0572d2d341f25e Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 00:20:22 +0100 Subject: [PATCH 06/21] Moved the content of Curve.hpp into Curve.h --- src/sfizz/Curve.h | 29 ++++++++++++++++++++++------- src/sfizz/Curve.hpp | 39 --------------------------------------- src/sfizz/SfzHelpers.h | 22 +++++++++++++++++----- 3 files changed, 39 insertions(+), 51 deletions(-) delete mode 100644 src/sfizz/Curve.hpp diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h index dff2f86e..c9a724f7 100644 --- a/src/sfizz/Curve.h +++ b/src/sfizz/Curve.h @@ -6,6 +6,8 @@ #pragma once #include "absl/types/span.h" +#include "MathHelpers.h" +#include "SfzHelpers.h" #include #include #include @@ -22,17 +24,29 @@ public: /** * @brief Compute the curve for integral x in domain [0:127] */ - float evalCC7(int value7) const; - + float evalCC7(int value7) const + { + return _points[clamp(value7, 0, 127)]; + } /** * @brief Compute the curve for real x in domain [0:127] */ - float evalCC7(float value7) const; + float evalCC7(float value7) const + { + value7 = clamp(value7, 0.0f, 127.0f); + int i1 = static_cast(value7); + int i2 = std::min(127, i1 + 1); + float mu = value7 - i1; + return _points[i1] + mu * (_points[i2] - _points[i1]); + } /** * @brief Compute the curve for real x in domain [0:1] */ - float evalNormalized(float value) const; + float evalNormalized(float value) const + { + return evalCC7(denormalize7Bits(value)); + } /** * @brief Kind of curve interpolator @@ -118,7 +132,10 @@ public: /** * @brief Get the number of slots. */ - unsigned getNumCurves() const; + unsigned getNumCurves() const + { + return static_cast(_curves.size()); + } private: std::vector> _curves; @@ -126,5 +143,3 @@ private: }; } // namespace sfz - -#include "Curve.hpp" diff --git a/src/sfizz/Curve.hpp b/src/sfizz/Curve.hpp deleted file mode 100644 index 1ebead18..00000000 --- a/src/sfizz/Curve.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#pragma once -#include "Curve.h" -#include "MathHelpers.h" - -namespace sfz -{ - -inline float Curve::evalCC7(int value7) const -{ - return _points[clamp(value7, 0, 127)]; -} - -inline float Curve::evalCC7(float value7) const -{ - value7 = clamp(value7, 0.0f, 127.0f); - int i1 = static_cast(value7); - int i2 = std::min(127, i1 + 1); - float mu = value7 - i1; - return _points[i1] + mu * (_points[i2] - _points[i1]); -} - -inline float Curve::evalNormalized(float value) const -{ - return evalCC7(127 * value); -} - -/// -inline unsigned CurveSet::getNumCurves() const -{ - return static_cast(_curves.size()); -} - -} // namespace sfz diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index a50c3428..4ff80155 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -13,6 +13,7 @@ #include "Macros.h" #include "Config.h" #include "MathHelpers.h" +#include "absl/meta/type_traits.h" namespace sfz { @@ -76,8 +77,20 @@ constexpr float centsFactor(T cents, T centsPerOctave = 1200) return std::pow(2.0f, static_cast(cents) / centsPerOctave); } +template::value, int> = 0> +constexpr T denormalize7Bits(float value) +{ + return static_cast(value * 127.0f); +} + +template +constexpr float normalize7Bits(T value) +{ + return static_cast(min(max(value, T{ 0 }), T{ 127 })) / 127.0f; +} + /** - * @brief Normalize a CC value between (T)0.0 and (T)1.0 + * @brief Normalize a CC value between 0.0 and 1.0 * * @tparam T * @param ccValue @@ -86,12 +99,11 @@ constexpr float centsFactor(T cents, T centsPerOctave = 1200) template constexpr float normalizeCC(T ccValue) { - static_assert(std::is_integral::value, "Requires an integral T"); - return static_cast(min(max(ccValue, static_cast(0)), static_cast(127))) / 127.0f; + return normalize7Bits(ccValue); } /** - * @brief Normalize a velocity between (T)0.0 and (T)1.0 + * @brief Normalize a velocity between 0.0 and 1.0 * * @tparam T * @param ccValue @@ -100,7 +112,7 @@ constexpr float normalizeCC(T ccValue) template constexpr float normalizeVelocity(T velocity) { - return normalizeCC(velocity); + return normalize7Bits(velocity); } From 84b58f82bd7bb0091a5c5c49761161b9409087bc Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 00:41:55 +0100 Subject: [PATCH 07/21] Debug mode did not compile --- src/sfizz/Curve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 355d660b..85d6b5d6 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -219,7 +219,7 @@ void CurveSet::addCurveFromHeader(absl::Span members) if (auto opt = readOpcode(opc->value, {0, 255})) curveIndex = *opt; else - DBG("Invalid value for curve index: " << opc.value); + DBG("Invalid value for curve index: " << opc->value); } #if 0 // potential sfizz extension From e58bf6f8cbbada959f7fb2fc75a15b750db699f5 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:09:30 +0100 Subject: [PATCH 08/21] Use linearRamp and correct a small bug in lerpFill --- src/sfizz/Curve.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 85d6b5d6..5f6511d9 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -6,6 +6,7 @@ #include "Curve.h" #include "Opcode.h" +#include "SIMDHelpers.h" #include "Debug.h" #include #include @@ -133,13 +134,18 @@ void Curve::fill(Interpolator itp, const bool fillStatus[NumValues]) void Curve::lerpFill(const bool fillStatus[NumValues]) { - for (int iCurr = 1; iCurr < NumValues - 1; ++iCurr) { - int iLeft, iRight; - iLeft = iCurr - 1; - for (iRight = iCurr + 1; iRight < 127 && !fillStatus[iRight]; ++iRight); + int left { 0 }; + int right { 1 }; + auto pointSpan = absl::MakeSpan(_points); - float mu = static_cast(iCurr - iLeft) / (iRight - iLeft); - _points[iCurr] = _points[iLeft] + mu * (_points[iRight] - _points[iLeft]); + while (right < NumValues) { + for (; right < NumValues && !fillStatus[right]; ++right); + const auto length = right - left; + if (length > 1) { + const float mu = (_points[right] - _points[left]) / length; + linearRamp(pointSpan.subspan(left + 1, length - 1), _points[left], mu); + } + left = right++; } } From d10536f07f88c825889fb9c5b20d3905141e24e3 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:09:40 +0100 Subject: [PATCH 09/21] Add tests for Curve --- tests/CMakeLists.txt | 1 + tests/CurveT.cpp | 196 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 tests/CurveT.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ed0d32a..eb28bb5c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ set(SFIZZ_TEST_SOURCES EventEnvelopesT.cpp MainT.cpp SynthT.cpp + CurveT.cpp RegionTriggersT.cpp FloatHelpersT.cpp ) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp new file mode 100644 index 00000000..b83a6b8d --- /dev/null +++ b/tests/CurveT.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/Curve.h" +#include "sfizz/Opcode.h" +#include "catch2/catch.hpp" +#include +using namespace Catch::literals; + +TEST_CASE("[Curve] Bipolar 0 to 1") +{ + auto curve = sfz::Curve::buildPredefinedCurve(0); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.0157).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(0.496).margin(1e-3) ); + REQUIRE( curve.evalCC7(85) == Approx(0.669).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 0.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.299).margin(1e-3) ); +} + +TEST_CASE("[Curve] Bipolar -1 to 1") +{ + auto curve = sfz::Curve::buildPredefinedCurve(1); + REQUIRE( curve.evalCC7(0) == -1.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(-0.9685).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(-0.00787).margin(1e-5) ); + REQUIRE( curve.evalCC7(85) == Approx(0.3386).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == -1.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(-0.402).margin(1e-3) ); +} + +TEST_CASE("[Curve] Bipolar 1 to 0") +{ + auto curve = sfz::Curve::buildPredefinedCurve(2); + REQUIRE( curve.evalCC7(0) == 1.0f ); + REQUIRE( curve.evalCC7(127) == 0.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.984).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(0.504).margin(1e-3) ); + REQUIRE( curve.evalCC7(85) == Approx(0.331).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 0.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.701).margin(1e-3) ); +} + +TEST_CASE("[Curve] Bipolar 1 to -1") +{ + auto curve = sfz::Curve::buildPredefinedCurve(3); + REQUIRE( curve.evalCC7(0) == 1.0f ); + REQUIRE( curve.evalCC7(127) == -1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.9685).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(0.00787).margin(1e-5) ); + REQUIRE( curve.evalCC7(85) == Approx(-0.3386).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(1.0f) == -1.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.402).margin(1e-3) ); +} + +TEST_CASE("[Curve] x**2") +{ + auto curve = sfz::Curve::buildPredefinedCurve(4); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.000248).margin(1e-5) ); + REQUIRE( curve.evalCC7(63) == Approx(0.246).margin(1e-3) ); + REQUIRE( curve.evalCC7(85) == Approx(0.448).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 0.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.09).margin(1e-3) ); +} + + +TEST_CASE("[Curve] sqrt(x)") +{ + auto curve = sfz::Curve::buildPredefinedCurve(5); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.125).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(0.704).margin(1e-3) ); + REQUIRE( curve.evalCC7(85) == Approx(0.818).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 0.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.5477).margin(1e-3) ); +} + +TEST_CASE("[Curve] sqrt(1-x)") +{ + auto curve = sfz::Curve::buildPredefinedCurve(6); + REQUIRE( curve.evalCC7(0) == 1.0f ); + REQUIRE( curve.evalCC7(127) == 0.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.992).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == Approx(0.710).margin(1e-3) ); + REQUIRE( curve.evalCC7(85) == Approx(0.575).margin(1e-3) ); + REQUIRE( curve.evalNormalized(0.0f) == 1.0f ); + REQUIRE( curve.evalNormalized(1.0f) == 0.0f ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.837).margin(1e-3) ); +} + +TEST_CASE("[Curve] Custom") +{ + auto curve = sfz::Curve::buildCurveFromHeader({ + { "v000", "0" }, + { "v063", "1" }, + { "v127", "0" } + }, sfz::Curve::Interpolator::Linear); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 0.0f ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(70) == Approx(0.891).margin(1e-3) ); +} + +TEST_CASE("[Curve] Custom 2") +{ + auto curve = sfz::Curve::buildCurveFromHeader({ + { "v063", "1" } + }, sfz::Curve::Interpolator::Linear); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(70) == 1.0f ); +} +TEST_CASE("[Curve] Custom 3") +{ + auto curve = sfz::Curve::buildCurveFromHeader({ + { "v063", "1" }, + { "v064", "0.5" } + }, sfz::Curve::Interpolator::Linear); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(70) == Approx(0.548).margin(1e-3) ); +} + + +TEST_CASE("[Curve] Add curves to CurveSet") +{ + sfz::CurveSet curveSet; + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0)); + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(2)); + REQUIRE( curveSet.getNumCurves() == 2 ); + REQUIRE( curveSet.getCurve(0).evalCC7(0) == 0.0f ); + REQUIRE( curveSet.getCurve(1).evalCC7(0) == 1.0f ); + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0), 1); + REQUIRE(curveSet.getNumCurves() == 2); + REQUIRE( curveSet.getCurve(1).evalCC7(0) == 0.0f ); + // Can't add an implicit curve after the explicit one + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0)); + REQUIRE(curveSet.getNumCurves() == 2); + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(2), 4); + REQUIRE(curveSet.getNumCurves() == 5); + REQUIRE( curveSet.getCurve(2).evalCC7(0) == 0.0f ); // Default "empty" curve + REQUIRE( curveSet.getCurve(3).evalCC7(0) == 0.0f ); // Default "empty" curve + REQUIRE( curveSet.getCurve(4).evalCC7(0) == 1.0f ); +} + +TEST_CASE("[Curve] Default CurveSet") +{ + auto curveSet = sfz::CurveSet::createPredefined(); + REQUIRE(curveSet.getNumCurves() == 7); + REQUIRE( curveSet.getCurve(0).evalNormalized(0.0f) == 0.0f ); + REQUIRE( curveSet.getCurve(0).evalNormalized(1.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(0).evalNormalized(0.3f) == Approx(0.299).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(1).evalNormalized(0.0f) == -1.0f ); + REQUIRE( curveSet.getCurve(1).evalNormalized(1.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(1).evalNormalized(0.3f) == Approx(-0.402).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(2).evalNormalized(0.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(2).evalNormalized(1.0f) == 0.0f ); + REQUIRE( curveSet.getCurve(2).evalNormalized(0.3f) == Approx(0.701).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(3).evalNormalized(0.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(3).evalNormalized(1.0f) == -1.0f ); + REQUIRE( curveSet.getCurve(3).evalNormalized(0.3f) == Approx(0.402).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(4).evalNormalized(0.0f) == 0.0f ); + REQUIRE( curveSet.getCurve(4).evalNormalized(1.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(4).evalNormalized(0.3f) == Approx(0.09).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(5).evalNormalized(0.0f) == 0.0f ); + REQUIRE( curveSet.getCurve(5).evalNormalized(1.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(5).evalNormalized(0.3f) == Approx(0.5477).margin(1e-3) ); + + REQUIRE( curveSet.getCurve(6).evalNormalized(0.0f) == 1.0f ); + REQUIRE( curveSet.getCurve(6).evalNormalized(1.0f) == 0.0f ); + REQUIRE( curveSet.getCurve(6).evalNormalized(0.3f) == Approx(0.837).margin(1e-3) ); +} From ea109f7896d3c9e78a439aceef1e0da4460e44f3 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:16:22 +0100 Subject: [PATCH 10/21] Correct a tidy warning --- src/sfizz/Curve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 5f6511d9..dbbe60bf 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -8,7 +8,7 @@ #include "Opcode.h" #include "SIMDHelpers.h" #include "Debug.h" -#include +#include "spline/spline.h" #include namespace sfz @@ -179,7 +179,7 @@ CurveSet CurveSet::createPredefined() CurveSet cs; cs._curves.reserve(16); - for (unsigned i = 0; i < Curve::NumPredefinedCurves; ++i) { + for (int i = 0; i < Curve::NumPredefinedCurves; ++i) { Curve curve = Curve::buildPredefinedCurve(i); cs._curves.emplace_back(new Curve(curve)); } From dc0040a46b8d6c3a9e795da278765145619f9f31 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:18:21 +0100 Subject: [PATCH 11/21] Static cast to int --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 816d0b26..8f7612ef 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -716,7 +716,7 @@ int sfz::Synth::getNumMasters() const noexcept } int sfz::Synth::getNumCurves() const noexcept { - return curves.getNumCurves(); + return static_cast(curves.getNumCurves()); } std::string sfz::Synth::exportMidnam(absl::string_view model) const From 9b349765dcbc8ffd72653b6f66fef2a23dba6228 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:18:40 +0100 Subject: [PATCH 12/21] The debug in the else branch stymies clang tidy --- src/sfizz/Curve.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index dbbe60bf..be81e67e 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -222,10 +222,11 @@ void CurveSet::addCurveFromHeader(absl::Span members) Curve::Interpolator itp = Curve::Interpolator::Linear; if (const Opcode* opc = findOpcode(hash("curve_index"))) { - if (auto opt = readOpcode(opc->value, {0, 255})) + if (auto opt = readOpcode(opc->value, {0, 255})) { curveIndex = *opt; - else + } else { DBG("Invalid value for curve index: " << opc->value); + } } #if 0 // potential sfizz extension From 4d52ce955fa5a7145d22d4cbeeb9fd82da5a7b89 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:21:51 +0100 Subject: [PATCH 13/21] Cast to int; value is bounded by NumValues --- src/sfizz/Curve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index be81e67e..132d67d4 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -44,7 +44,7 @@ Curve Curve::buildCurveFromHeader( if (!valueOpt) continue; - setPoint(index, *valueOpt); + setPoint(static_cast(index), *valueOpt); } curve.fill(itp, fillStatus); From 9606ea0dabfa5d6a4173332ae65f0fed2c2097a5 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:22:04 +0100 Subject: [PATCH 14/21] Change count to int --- src/sfizz/Curve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 132d67d4..e1fa0592 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -153,7 +153,7 @@ void Curve::splineFill(const bool fillStatus[NumValues]) { std::array x; std::array y; - unsigned count = 0; + int count = 0; for (unsigned i = 0; i < NumValues; ++i) { if (fillStatus[i]) { From 2d924bd9be5b087d1645373f1fea9820cd13191f Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:22:46 +0100 Subject: [PATCH 15/21] Add curve to clang-tidy --- scripts/run_clang_tidy.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/run_clang_tidy.sh b/scripts/run_clang_tidy.sh index 7431d3f5..782c1e0f 100755 --- a/scripts/run_clang_tidy.sh +++ b/scripts/run_clang_tidy.sh @@ -2,6 +2,7 @@ clang-tidy \ src/sfizz/ADSREnvelope.cpp \ + src/sfizz/Curve.cpp \ src/sfizz/Effects.cpp \ src/sfizz/EQPool.cpp \ src/sfizz/EventEnvelopes.cpp \ @@ -28,5 +29,5 @@ clang-tidy \ vst/SfizzVstEditor.cpp \ vst/SfizzVstState.cpp \ -- -Iexternal/abseil-cpp -Isrc/external -Isrc/external/pugixml/src \ - -Isrc/sfizz -Isrc \ + -Isrc/sfizz -Isrc -Isrc/external/spline \ -Ivst -Ivst/external/VST_SDK/VST3_SDK -Ivst/external/VST_SDK/VST3_SDK/vstgui4 -DNDEBUG From be027df94e43d62738377f7c3b4fd0b2c8274f93 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 02:26:50 +0100 Subject: [PATCH 16/21] Add an out-of-bound curve test --- tests/CurveT.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index b83a6b8d..02158632 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -149,6 +149,10 @@ TEST_CASE("[Curve] Add curves to CurveSet") REQUIRE( curveSet.getNumCurves() == 2 ); REQUIRE( curveSet.getCurve(0).evalCC7(0) == 0.0f ); REQUIRE( curveSet.getCurve(1).evalCC7(0) == 1.0f ); + // Out of bound curve defaults to linear + REQUIRE( curveSet.getCurve(2).evalCC7(0) == 0.0f ); + REQUIRE( curveSet.getCurve(2).evalCC7(127) == 1.0f ); + // Change a curve in a position curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0), 1); REQUIRE(curveSet.getNumCurves() == 2); REQUIRE( curveSet.getCurve(1).evalCC7(0) == 0.0f ); From 53d3c51de5446d1cd1c5836a897a535e5c2406d6 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 10:15:57 +0100 Subject: [PATCH 17/21] State the fallthrough --- src/sfizz/Curve.cpp | 1 + tests/CurveT.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index e1fa0592..b54807ec 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -64,6 +64,7 @@ Curve Curve::buildPredefinedCurve(int index) switch (index) { default: ASSERTFALSE; + // fallthrough case 0: curve = buildBipolar(0, 1); break; diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 02158632..0ede91ff 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -127,6 +127,7 @@ TEST_CASE("[Curve] Custom 2") REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); REQUIRE( curve.evalCC7(70) == 1.0f ); } + TEST_CASE("[Curve] Custom 3") { auto curve = sfz::Curve::buildCurveFromHeader({ From b4ddd2aabe2fbc4230071a3cec79592a712b9d38 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 10:27:20 +0100 Subject: [PATCH 18/21] Add another test case --- tests/CurveT.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 0ede91ff..b2ea902e 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -123,8 +123,8 @@ TEST_CASE("[Curve] Custom 2") }, sfz::Curve::Interpolator::Linear); REQUIRE( curve.evalCC7(0) == 0.0f ); REQUIRE( curve.evalCC7(127) == 1.0f ); - REQUIRE( curve.evalCC7(63) == 1.0f ); REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == 1.0f ); REQUIRE( curve.evalCC7(70) == 1.0f ); } @@ -136,11 +136,26 @@ TEST_CASE("[Curve] Custom 3") }, sfz::Curve::Interpolator::Linear); REQUIRE( curve.evalCC7(0) == 0.0f ); REQUIRE( curve.evalCC7(127) == 1.0f ); - REQUIRE( curve.evalCC7(63) == 1.0f ); REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + REQUIRE( curve.evalCC7(64) == 0.5f ); REQUIRE( curve.evalCC7(70) == Approx(0.548).margin(1e-3) ); } +TEST_CASE("[Curve] Custom 4") +{ + auto curve = sfz::Curve::buildCurveFromHeader({ + { "v063", "1" }, + { "v065", "0.5" } + }, sfz::Curve::Interpolator::Linear); + REQUIRE( curve.evalCC7(0) == 0.0f ); + REQUIRE( curve.evalCC7(127) == 1.0f ); + REQUIRE( curve.evalCC7(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + REQUIRE( curve.evalCC7(64) == Approx(0.75).margin(1e-3) ); + REQUIRE( curve.evalCC7(65) == 0.5f ); + REQUIRE( curve.evalCC7(70) == Approx(0.54).margin(1e-3) ); +} TEST_CASE("[Curve] Add curves to CurveSet") { From 61314db79d19a255ca2f735b7f1faf85eb72deca Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 10:43:42 +0100 Subject: [PATCH 19/21] do while trick on debug --- src/sfizz/Curve.cpp | 5 ++--- src/sfizz/Debug.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index b54807ec..ee9c5af2 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -223,11 +223,10 @@ void CurveSet::addCurveFromHeader(absl::Span members) Curve::Interpolator itp = Curve::Interpolator::Linear; if (const Opcode* opc = findOpcode(hash("curve_index"))) { - if (auto opt = readOpcode(opc->value, {0, 255})) { + if (auto opt = readOpcode(opc->value, {0, 255})) curveIndex = *opt; - } else { + else DBG("Invalid value for curve index: " << opc->value); - } } #if 0 // potential sfizz extension diff --git a/src/sfizz/Debug.h b/src/sfizz/Debug.h index 6ef5a8b7..72d239bc 100644 --- a/src/sfizz/Debug.h +++ b/src/sfizz/Debug.h @@ -45,7 +45,7 @@ ASSERTFALSE // Debug message -#define DBG(ostream) std::cerr << ostream << '\n' +#define DBG(ostream) do { std::cerr << ostream << '\n'; } while (0) #else // NDEBUG From 381b96025204eb58511d6c1c3172305d21f223a7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 15 Mar 2020 11:08:55 +0100 Subject: [PATCH 20/21] Fix all Debug.h block macros everywhere --- src/sfizz/AudioBuffer.h | 10 +++++----- src/sfizz/Debug.h | 26 ++++++++++++++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/sfizz/AudioBuffer.h b/src/sfizz/AudioBuffer.h index 3bc919d0..dcd80588 100644 --- a/src/sfizz/AudioBuffer.h +++ b/src/sfizz/AudioBuffer.h @@ -85,7 +85,7 @@ public: */ iterator channelWriter(size_t channelIndex) { - ASSERT(channelIndex < numChannels) + ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) return buffers[channelIndex]->data(); @@ -100,7 +100,7 @@ public: */ iterator channelWriterEnd(size_t channelIndex) { - ASSERT(channelIndex < numChannels) + ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) return buffers[channelIndex]->end(); @@ -115,7 +115,7 @@ public: */ const_iterator channelReader(size_t channelIndex) const { - ASSERT(channelIndex < numChannels) + ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) return buffers[channelIndex]->data(); @@ -130,7 +130,7 @@ public: */ const_iterator channelReaderEnd(size_t channelIndex) const { - ASSERT(channelIndex < numChannels) + ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) return buffers[channelIndex]->end(); @@ -145,7 +145,7 @@ public: */ absl::Span getSpan(size_t channelIndex) const { - ASSERT(channelIndex < numChannels) + ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) return { buffers[channelIndex]->data(), buffers[channelIndex]->size() }; diff --git a/src/sfizz/Debug.h b/src/sfizz/Debug.h index 72d239bc..8479b6ec 100644 --- a/src/sfizz/Debug.h +++ b/src/sfizz/Debug.h @@ -14,16 +14,16 @@ // Break in source code #if (__x86_64__ || __i386__) #define ASSERTFALSE \ - { \ + do { \ std::cerr << "Assert failed at " << __FILE__ << ":" << __LINE__ << '\n'; \ __asm__("int3"); \ - } + } while (0) #elif (__arm__ || __aarch64__) #define ASSERTFALSE \ - { \ + do { \ std::cerr << "Assert failed at " << __FILE__ << ":" << __LINE__ << '\n'; \ __builtin_trap(); \ - } + } while (0) #endif #elif (_WIN32 || _WIN64) @@ -31,26 +31,28 @@ #pragma intrinsic(__debugbreak) #endif #define ASSERTFALSE \ - { \ + do { \ std::cerr << "Assert failed at " << __FILE__ << ":" << __LINE__ << '\n'; \ __debugbreak(); \ - } + } while (0) #else -#define ASSERTFALSE +#define ASSERTFALSE do {} while (0) #endif // Assert stuff #define ASSERT(expression) \ - if (!(expression)) \ - ASSERTFALSE + do { \ + if (!(expression)) \ + ASSERTFALSE \ + while (0) // Debug message #define DBG(ostream) do { std::cerr << ostream << '\n'; } while (0) #else // NDEBUG -#define ASSERTFALSE -#define ASSERT(expression) -#define DBG(ostream) +#define ASSERTFALSE do {} while (0) +#define ASSERT(expression) do {} while (0) +#define DBG(ostream) do {} while (0) #endif From bf5d50e8e04872c58747664c8b2a576101838c92 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 15 Mar 2020 11:24:30 +0100 Subject: [PATCH 21/21] Additional test case --- tests/CurveT.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index b2ea902e..9c8b054b 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -157,6 +157,23 @@ TEST_CASE("[Curve] Custom 4") REQUIRE( curve.evalCC7(70) == Approx(0.54).margin(1e-3) ); } +TEST_CASE("[Curve] Custom 5") +{ + auto curve = sfz::Curve::buildCurveFromHeader({ + { "v000", "1" }, + { "v064", "0.9" }, + { "v100", "0.9" }, + { "v127", "0" } + }, sfz::Curve::Interpolator::Linear); + REQUIRE( curve.evalCC7(0) == 1.0f ); + REQUIRE( curve.evalCC7(15) == Approx(0.977).margin(1e-3) ); + REQUIRE( curve.evalCC7(64) == 0.9f ); + REQUIRE( curve.evalCC7(90) == 0.9f ); + REQUIRE( curve.evalCC7(100) == 0.9f ); + REQUIRE( curve.evalCC7(110) == Approx(0.567).margin(1e-3) ); + REQUIRE( curve.evalCC7(127) == 0.0f ); +} + TEST_CASE("[Curve] Add curves to CurveSet") { sfz::CurveSet curveSet;