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; +}