Add curves

This commit is contained in:
Jean Pierre Cimalando 2020-03-03 13:11:31 +01:00 committed by Paul Fd
parent d12aaa9f48
commit 357b17d55f
10 changed files with 662 additions and 2 deletions

View file

@ -50,6 +50,7 @@ else()
endif() endif()
endif() endif()
# If we build with Clang use libc++ # If we build with Clang use libc++
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT ANDROID) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT ANDROID)
set(USE_LIBCPP ON CACHE BOOL "Use libc++ with clang") 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") add_library(sfizz-pugixml STATIC "src/external/pugixml/src/pugixml.cpp")
target_include_directories(sfizz-pugixml PUBLIC "src/external/pugixml/src") 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) include (CheckLibraryExists)
if (UNIX AND NOT APPLE) if (UNIX AND NOT APPLE)
check_library_exists(atomic __atomic_load "" LIBATOMIC_FOUND) check_library_exists(atomic __atomic_load "" LIBATOMIC_FOUND)

View file

@ -14,6 +14,7 @@ set (SFIZZ_SOURCES
sfizz/FloatEnvelopes.cpp sfizz/FloatEnvelopes.cpp
sfizz/Logger.cpp sfizz/Logger.cpp
sfizz/SfzFilter.cpp sfizz/SfzFilter.cpp
sfizz/Curve.cpp
sfizz/Effects.cpp sfizz/Effects.cpp
sfizz/effects/Nothing.cpp sfizz/effects/Nothing.cpp
sfizz/effects/Filter.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 .)
target_include_directories (sfizz_static PUBLIC external) target_include_directories (sfizz_static PUBLIC external)
target_link_libraries (sfizz_static PUBLIC absl::strings absl::span) 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") set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp")
if (WIN32) if (WIN32)
target_compile_definitions (sfizz_static PRIVATE _USE_MATH_DEFINES) 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_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 .)
target_include_directories (sfizz_shared PRIVATE external) 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) if (WIN32)
target_compile_definitions (sfizz_shared PRIVATE _USE_MATH_DEFINES) target_compile_definitions (sfizz_shared PRIVATE _USE_MATH_DEFINES)
endif() endif()

29
src/external/spline/LICENSE vendored Normal file
View file

@ -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.

77
src/external/spline/spline/spline.cpp vendored Normal file
View file

@ -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 <cassert>
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<double> 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<int>(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);
}

46
src/external/spline/spline/spline.h vendored Normal file
View file

@ -0,0 +1,46 @@
/*==============================================================================
Copyright 2018 by Roland Rabien, Devin Lane
For more information visit www.rabiensoftware.com
==============================================================================*/
#pragma once
#include <vector>
/** 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<Element> elements;
};

249
src/sfizz/Curve.cpp Normal file
View file

@ -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 <spline/spline.h>
#include <cmath>
namespace sfz
{
static const Curve defaultCurve = Curve::buildBipolar(0.0, 1.0);
Curve Curve::buildCurveFromHeader(
absl::Span<const Opcode> members, Interpolator itp, bool limit)
{
Curve curve;
bool fillStatus[NumValues] = {};
const Range<float> 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<float>(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<float>(iCurr - iLeft) / (iRight - iLeft);
_points[iCurr] = _points[iLeft] + mu * (_points[iRight] - _points[iLeft]);
}
}
void Curve::splineFill(const bool fillStatus[NumValues])
{
std::array<double, NumValues> x;
std::array<double, NumValues> 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<Curve>* 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<unsigned>(explicitIndex);
if (index >= _curves.size())
_curves.resize(index + 1);
_useExplicitIndexing = true;
slot = &_curves[index];
}
slot->reset(new Curve(curve));
}
void CurveSet::addCurveFromHeader(absl::Span<const Opcode> 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<int>(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

125
src/sfizz/Curve.h Normal file
View file

@ -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 <array>
#include <vector>
#include <memory>
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 <curve> header
*
* @param members contents of the <curve> 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<const Opcode> 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<float, NumValues> _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 <curve> header
*
* @param members contents of the <curve> header
*/
void addCurveFromHeader(absl::Span<const Opcode> members);
/**
* @brief Get a curve given its index
*/
const Curve& getCurve(unsigned index) const;
private:
std::vector<std::unique_ptr<Curve>> _curves;
bool _useExplicitIndexing = false;
};
} // namespace sfz
#include "Curve.hpp"

33
src/sfizz/Curve.hpp Normal file
View file

@ -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<int>(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

View file

@ -56,5 +56,7 @@ target_link_libraries(eq_apply PRIVATE sfizz::sfizz)
add_executable(filter_apply Filter.cpp) add_executable(filter_apply Filter.cpp)
target_link_libraries(filter_apply PRIVATE sfizz::sfizz) 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) file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests)

94
tests/PlotCurve.cpp Normal file
View file

@ -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 <iostream>
#include <cmath>
//==============================================================================
/**
* @brief Print usage information
*/
static void usage()
{
std::cerr << "Usage: sfizz_plot_curve <index> [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<sfz::Opcode>& 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;
}