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/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 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/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/Curve.cpp b/src/sfizz/Curve.cpp new file mode 100644 index 00000000..ee9c5af2 --- /dev/null +++ b/src/sfizz/Curve.cpp @@ -0,0 +1,257 @@ +// 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 "SIMDHelpers.h" +#include "Debug.h" +#include "spline/spline.h" +#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) { + if (opc.lettersOnlyHash != hash("v&")) + continue; + + unsigned index = opc.parameters.back(); + if (index >= NumValues) + continue; + + auto valueOpt = readOpcode(opc.value, fullRange); + if (!valueOpt) + continue; + + setPoint(static_cast(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; + // fallthrough + 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]) +{ + int left { 0 }; + int right { 1 }; + auto pointSpan = absl::MakeSpan(_points); + + 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++; + } +} + +void Curve::splineFill(const bool fillStatus[NumValues]) +{ + std::array x; + std::array y; + int count = 0; + + for (unsigned i = 0; i < NumValues; ++i) { + if (fillStatus[i]) { + x[count] = i; + y[count] = _points[i]; + ++count; + } + } + + if (count < 3) + return lerpFill(fillStatus); + + 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 (int 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..c9a724f7 --- /dev/null +++ b/src/sfizz/Curve.h @@ -0,0 +1,145 @@ +// 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 "MathHelpers.h" +#include "SfzHelpers.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 + { + return _points[clamp(value7, 0, 127)]; + } + /** + * @brief Compute the curve for real x in domain [0:127] + */ + 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 + { + return evalCC7(denormalize7Bits(value)); + } + + /** + * @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; + + /** + * @brief Get the number of slots. + */ + unsigned getNumCurves() const + { + return static_cast(_curves.size()); + } + +private: + std::vector> _curves; + bool _useExplicitIndexing = false; +}; + +} // namespace sfz diff --git a/src/sfizz/Debug.h b/src/sfizz/Debug.h index 6ef5a8b7..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) std::cerr << ostream << '\n' +#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 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); } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b02ebd00..8f7612ef 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 static_cast(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 }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 454f718f..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 ) @@ -56,5 +57,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/CurveT.cpp b/tests/CurveT.cpp new file mode 100644 index 00000000..9c8b054b --- /dev/null +++ b/tests/CurveT.cpp @@ -0,0 +1,233 @@ +// 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(2) == Approx(0.032).margin(1e-3) ); + REQUIRE( curve.evalCC7(63) == 1.0f ); + 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(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] 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; + 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 ); + // 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 ); + // 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) ); +} diff --git a/tests/PlotCurve.cpp b/tests/PlotCurve.cpp new file mode 100644 index 00000000..5ca42c28 --- /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 == "curve") + 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; +}