diff --git a/benchmarks/BM_envelopes.cpp b/benchmarks/BM_envelopes.cpp index 836d6b0a..1fa3ae91 100644 --- a/benchmarks/BM_envelopes.cpp +++ b/benchmarks/BM_envelopes.cpp @@ -10,7 +10,7 @@ #include #include #include -#include "SfzHelpers.h" +#include "ModifierHelpers.h" #include "absl/types/span.h" class EnvelopeFixture : public benchmark::Fixture { diff --git a/src/sfizz/CCMap.h b/src/sfizz/CCMap.h index 09e8830c..1b44cf8c 100644 --- a/src/sfizz/CCMap.h +++ b/src/sfizz/CCMap.h @@ -44,11 +44,11 @@ public: */ const ValueType& getWithDefault(int index) const noexcept { - auto it = absl::c_lower_bound(container, index, CCValuePairComparator{}); + auto it = absl::c_lower_bound(container, index, CCDataComparator{}); if (it == container.end() || it->cc != index) { return defaultValue; } else { - return it->value; + return it->data; } } @@ -60,12 +60,12 @@ public: */ ValueType& operator[](const int& index) noexcept { - auto it = absl::c_lower_bound(container, index, CCValuePairComparator{}); + auto it = absl::c_lower_bound(container, index, CCDataComparator{}); if (it == container.end() || it->cc != index) { auto inserted = container.insert(it, { index, defaultValue }); - return inserted->value; + return inserted->data; } else { - return it->value; + return it->data; } } @@ -85,16 +85,16 @@ public: */ bool contains(int index) const noexcept { - return absl::c_binary_search(container, index, CCValuePairComparator{}); + return absl::c_binary_search(container, index, CCDataComparator{}); } - typename std::vector>::const_iterator begin() const { return container.cbegin(); } - typename std::vector>::const_iterator end() const { return container.cend(); } + typename std::vector>::const_iterator begin() const { return container.cbegin(); } + typename std::vector>::const_iterator end() const { return container.cend(); } private: // typename std::vector>::iterator begin() { return container.begin(); } // typename std::vector>::iterator end() { return container.end(); } const ValueType defaultValue; - std::vector> container; + std::vector> container; LEAK_DETECTOR(CCMap); }; } diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 727a125f..48aeb70f 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -56,6 +56,7 @@ namespace config { constexpr size_t powerHistoryLength { 16 }; constexpr float voiceStealingThreshold { 0.00001f }; constexpr uint16_t numCCs { 512 }; + constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; constexpr int filtersInPool { maxVoices * 2 }; constexpr int filtersPerVoice { 2 }; diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 44aa3c24..f349efad 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -196,6 +196,12 @@ void CurveSet::addCurve(const Curve& curve, int explicitIndex) { std::unique_ptr* slot; + if (explicitIndex < -1) + return; + + if (explicitIndex >= config::maxCurves) + return; + if (explicitIndex == -1) { if (_useExplicitIndexing) return; // reject implicit indices if any were explicit before diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h index ad9667a3..829a2d8b 100644 --- a/src/sfizz/Curve.h +++ b/src/sfizz/Curve.h @@ -45,7 +45,7 @@ public: */ float evalNormalized(float value) const { - return evalCC7(denormalize7Bits(value)); + return evalCC7(value * 127.0f); } /** diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 158da597..8506d90f 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -58,7 +58,6 @@ namespace Default constexpr Range midi7Range { 0, 127 }; constexpr Range normalizedRange { 0.0f, 1.0f }; constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; - constexpr float zeroModifier { 0.0f }; // Wavetable oscillator constexpr float oscillatorPhase { 0.0 }; @@ -82,6 +81,9 @@ namespace Default // Region logic: MIDI conditions constexpr Range channelRange { 1, 16 }; constexpr Range midiChannelRange { 0, 15 }; + constexpr Range stepCCRange { 0, 127 }; + constexpr Range smoothCCRange { 0, 127 }; + constexpr Range curveCCRange { 0, 255 }; constexpr Range ccNumberRange { 0, config::numCCs }; constexpr auto ccValueRange = normalizedRange; constexpr Range bendRange = { -8192, 8192 }; @@ -108,17 +110,21 @@ namespace Default constexpr float volume { 0.0f }; constexpr Range volumeRange { -144.0, 6.0 }; constexpr Range volumeCCRange { -144.0, 48.0 }; + constexpr Range volumeStepRange { 0, 48.0 }; constexpr float amplitude { 100.0 }; constexpr Range amplitudeRange { 0.0, 100.0 }; constexpr float pan { 0.0 }; constexpr Range panRange { -100.0, 100.0 }; constexpr Range panCCRange { -200.0, 200.0 }; + constexpr Range panStepRange { 0.0, 200.0 }; constexpr float position { 0.0 }; constexpr Range positionRange { -100.0, 100.0 }; constexpr Range positionCCRange { -200.0, 200.0 }; + constexpr Range positionStepRange { 0.0, 200.0 }; constexpr float width { 100.0 }; constexpr Range widthRange { -100.0, 100.0 }; constexpr Range widthCCRange { -200.0, 200.0 }; + constexpr Range widthStepRange { 0.0, 200.0 }; constexpr uint8_t ampKeycenter { 60 }; constexpr float ampKeytrack { 0.0 }; constexpr Range ampKeytrackRange { -96, 12 }; @@ -194,6 +200,7 @@ namespace Default constexpr int tune { 0 }; constexpr Range tuneRange { -9600, 9600 }; // ±100 in SFZv1, more in ARIA constexpr Range tuneCCRange { -9600, 9600 }; + constexpr Range tuneStepRange { 0, 9600 }; constexpr Range bendBoundRange { -9600, 9600 }; constexpr Range bendStepRange { 1, 1200 }; constexpr int bendUp { 200 }; // No range here because the bounds can be inverted diff --git a/src/sfizz/EGDescription.h b/src/sfizz/EGDescription.h index ee44f754..e64fa642 100644 --- a/src/sfizz/EGDescription.h +++ b/src/sfizz/EGDescription.h @@ -50,10 +50,10 @@ namespace sfz { * @param value * @return float */ -inline float ccSwitchedValue(const MidiState& state, const absl::optional>& ccSwitch, float value) noexcept +inline float ccSwitchedValue(const MidiState& state, const absl::optional>& ccSwitch, float value) noexcept { if (ccSwitch) - return value + ccSwitch->value * state.getCCValue(ccSwitch->cc); + return value + ccSwitch->data * state.getCCValue(ccSwitch->cc); else return value; } @@ -80,13 +80,13 @@ struct EGDescription { float vel2sustain { Default::vel2sustain }; int vel2depth { Default::depth }; - absl::optional> ccAttack; - absl::optional> ccDecay; - absl::optional> ccDelay; - absl::optional> ccHold; - absl::optional> ccRelease; - absl::optional> ccStart; - absl::optional> ccSustain; + absl::optional> ccAttack; + absl::optional> ccDecay; + absl::optional> ccDelay; + absl::optional> ccHold; + absl::optional> ccRelease; + absl::optional> ccStart; + absl::optional> ccSustain; /** * @brief Get the attack with possibly a CC modifier and a velocity modifier diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 65d5866c..adfdfec7 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -29,17 +29,17 @@ void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels // Setup the modulated values lastFrequency = baseFrequency; for (const auto& mod : description.frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.value; + lastFrequency += midiState.getCCValue(mod.cc) * mod.data; lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); lastBandwidth = baseBandwidth; for (const auto& mod : description.bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.value; + lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); lastGain = baseGain; for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain += midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the EQ @@ -62,17 +62,17 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF // For now we take the last value lastFrequency = baseFrequency; for (const auto& mod : description->frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.value; + lastFrequency += midiState.getCCValue(mod.cc) * mod.data; lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); lastBandwidth = baseBandwidth; for (const auto& mod : description->bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.value; + lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); lastGain = baseGain; for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain += midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); if (lastGain == 0.0f) { diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 9e4f9091..3b61276f 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -41,17 +41,17 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num // Setup the modulated values lastCutoff = baseCutoff; for (const auto& mod : description.cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value); + lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); lastResonance = baseResonance; for (const auto& mod : description.resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.value; + lastResonance += midiState.getCCValue(mod.cc) * mod.data; lastResonance = Default::filterResonanceRange.clamp(lastResonance); lastGain = baseGain; for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain += midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the filter @@ -71,17 +71,17 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned // TODO: the template deduction could be automatic here? lastCutoff = baseCutoff; for (const auto& mod : description->cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value); + lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); lastResonance = baseResonance; for (const auto& mod : description->resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.value; + lastResonance += midiState.getCCValue(mod.cc) * mod.data; lastResonance = Default::filterResonanceRange.clamp(lastResonance); lastGain = baseGain; for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain += midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); diff --git a/src/sfizz/Macros.h b/src/sfizz/Macros.h index 3769b34d..024dfe24 100644 --- a/src/sfizz/Macros.h +++ b/src/sfizz/Macros.h @@ -16,3 +16,10 @@ #define CXX11_MOVE(x) std::move(x) #endif +#if __cplusplus >= 201703L +#define IF_CONSTEXPR if constexpr +#else +#define IF_CONSTEXPR if +#endif + + diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index 9fa734f1..a70446ca 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -15,26 +15,27 @@ #include #include #include +#include -template +template constexpr T max(T op1, T op2) { return op1 > op2 ? op1 : op2; } -template +template constexpr T max(T op1, Args... rest) { return max(op1, max(rest...)); } -template +template constexpr T min(T op1, T op2) { return op1 > op2 ? op2 : op1; } -template +template constexpr T min(T op1, Args... rest) { return min(op1, min(rest...)); @@ -46,7 +47,7 @@ constexpr T min(T op1, Args... rest) * @param op * @return T */ -template +template constexpr T power2(T in) { return in * in; @@ -111,8 +112,8 @@ constexpr Type mag2db(Type in) * */ namespace Random { - static std::random_device randomDevice; - static std::minstd_rand randomGenerator { randomDevice() }; +static std::random_device randomDevice; +static std::minstd_rand randomGenerator { randomDevice() }; } // namespace Random /** @@ -135,42 +136,42 @@ inline float midiNoteFrequency(const int noteNumber) * @param hi * @return T */ -template -constexpr T clamp( T v, T lo, T hi ) +template +constexpr T clamp(T v, T lo, T hi) { return max(min(v, hi), lo); } -template +template inline CXX14_CONSTEXPR void incrementAll(T& only) { only += Increment; } -template +template inline CXX14_CONSTEXPR void incrementAll(T& first, Args&... rest) { first += Increment; incrementAll(rest...); } -template +template constexpr ValueType linearInterpolation(ValueType left, ValueType right, ValueType leftCoeff, ValueType rightCoeff) { return left * leftCoeff + right * rightCoeff; } -template +template constexpr Type pi() { return static_cast(3.141592653589793238462643383279502884); }; -template +template constexpr Type twoPi() { return pi() * 2; }; -template +template constexpr Type piTwo() { return pi() / 2; }; -template +template constexpr Type piFour() { return pi() / 4; }; -template +template constexpr Type sqrtTwo() { return static_cast(1.414213562373095048801688724209698078569671875376948073176); }; -template +template constexpr Type sqrtTwoInv() { return static_cast(0.707106781186547524400844362104849039284835937688474036588); }; /** @@ -205,23 +206,23 @@ inline Fraction::operator float() const noexcept template struct FP_traits; -template <> struct FP_traits -{ +template <> +struct FP_traits { typedef double type; typedef uint64_t same_size_int; static_assert(sizeof(type) == sizeof(same_size_int), - "Unexpected size of floating point type"); + "Unexpected size of floating point type"); static constexpr int e_bits = 11; static constexpr int m_bits = 52; static constexpr int e_offset = -1023; }; -template <> struct FP_traits -{ +template <> +struct FP_traits { typedef float type; typedef uint32_t same_size_int; static_assert(sizeof(type) == sizeof(same_size_int), - "Unexpected size of floating point type"); + "Unexpected size of floating point type"); static constexpr int e_bits = 8; static constexpr int m_bits = 23; static constexpr int e_offset = -127; @@ -237,7 +238,10 @@ template inline bool fp_sign(F x) { typedef FP_traits T; - union { F real; typename T::same_size_int integer; } u; + union { + F real; + typename T::same_size_int integer; + } u; u.real = x; return ((u.integer >> (T::e_bits + T::m_bits)) & 1) != 0; } @@ -254,7 +258,10 @@ template inline int fp_exponent(F x) { typedef FP_traits T; - union { F real; typename T::same_size_int integer; } u; + union { + F real; + typename T::same_size_int integer; + } u; u.real = x; int ex = (u.integer >> T::m_bits) & ((1u << T::e_bits) - 1); return ex + T::e_offset; @@ -269,10 +276,13 @@ template inline Fraction fp_mantissa(F x) { typedef FP_traits T; - union { F real; typename T::same_size_int integer; } u; + union { + F real; + typename T::same_size_int integer; + } u; u.real = x; Fraction f; - f.den = uint64_t{1} << T::m_bits; + f.den = uint64_t { 1 } << T::m_bits; f.num = u.integer & (f.den - 1); return f; } @@ -287,10 +297,11 @@ inline F fp_from_parts(bool sgn, int ex, uint64_t mant) { typedef FP_traits T; typedef typename T::same_size_int I; - union { F real; I integer; } u; - u.integer = mant | - (static_cast(ex - T::e_offset) << T::m_bits) | - (static_cast(sgn) << (T::e_bits + T::m_bits)); + union { + F real; + I integer; + } u; + u.integer = mant | (static_cast(ex - T::e_offset) << T::m_bits) | (static_cast(sgn) << (T::e_bits + T::m_bits)); return u.real; } @@ -328,3 +339,20 @@ bool isValidAudio(absl::Span span) return true; } + +class ScopedRoundingMode { +public: + ScopedRoundingMode() = delete; + ScopedRoundingMode(int newRoundingMode) + : savedFloatMode(std::fegetround()) + { + std::fesetround(newRoundingMode); + } + ~ScopedRoundingMode() + { + std::fesetround(savedFloatMode); + } + +private: + const int savedFloatMode; +}; diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h new file mode 100644 index 00000000..8e01610a --- /dev/null +++ b/src/sfizz/ModifierHelpers.h @@ -0,0 +1,244 @@ +#pragma once + +#include "Range.h" +#include "Defaults.h" +#include "SfzHelpers.h" +#include "Resources.h" +#include "absl/types/span.h" + +namespace sfz { + +/** + * @brief Compute a crossfade in value with respect to a crossfade range (note, velocity, cc, ...) + */ +template +float crossfadeIn(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value < crossfadeRange.getStart()) + return 0.0f; + + const auto length = static_cast(crossfadeRange.length()); + if (length == 0.0f) + return 1.0f; + + else if (value < crossfadeRange.getEnd()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; + if (curve == SfzCrossfadeCurve::power) + return sqrt(crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return crossfadePosition; + } + + return 1.0f; +} + +/** + * @brief Compute a crossfade out value with respect to a crossfade range (note, velocity, cc, ...) + */ +template +float crossfadeOut(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value > crossfadeRange.getEnd()) + return 0.0f; + + const auto length = static_cast(crossfadeRange.length()); + if (length == 0.0f) + return 1.0f; + + else if (value > crossfadeRange.getStart()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; + if (curve == SfzCrossfadeCurve::power) + return std::sqrt(1 - crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return 1 - crossfadePosition; + } + + return 1.0f; +} + +template +void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + if (envelope.size() == 0) + return; + + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = lambda(events[0].value); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto step = (lambda(events[i].value) - lastValue) / length; + lastValue = linearRamp(envelope.subspan(lastDelay, length), lastValue, step); + lastDelay += length; + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + ASSERT(step != 0.0); + + if (envelope.size() == 0) + return; + + auto quantize = [step](float value) -> float { + return std::trunc(value / step) * step; + }; + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = quantize(lambda(events[0].value)); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto nextValue = quantize(lambda(events[i].value)); + const auto difference = std::abs(nextValue - lastValue); + const auto length = min(events[i].delay, maxDelay) - lastDelay; + + if (difference < step) { + fill(envelope.subspan(lastDelay, length), lastValue); + lastValue = nextValue; + lastDelay += length; + continue; + } + + const auto numSteps = static_cast(difference / step); + const auto stepLength = static_cast(length / numSteps); + for (int i = 0; i < numSteps; ++i) { + fill(envelope.subspan(lastDelay, stepLength), lastValue); + lastValue += lastValue <= nextValue ? step : -step; + lastDelay += stepLength; + } + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + + if (envelope.size() == 0) + return; + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = lambda(events[0].value); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto nextValue = lambda(events[i].value); + const auto step = std::exp((std::log(nextValue) - std::log(lastValue)) / length); + multiplicativeRamp(envelope.subspan(lastDelay, length), lastValue, step); + lastValue = nextValue; + lastDelay += length; + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + ASSERT(step != 0.0f); + + ScopedRoundingMode roundingMode { Round ? FE_TONEAREST : FE_TOWARDZERO }; + + if (envelope.size() == 0) + return; + const auto maxDelay = static_cast(envelope.size() - 1); + const auto logStep = std::log(step); + // If we assume that a = b.q^r for b in (1, q) then + // log a log b + // ----- = ----- + r + // log q log q + // and log(b)\log(q) is between 0 and 1. + auto quantize = [logStep](float value) -> float { + return std::exp(logStep * std::rint(std::log(value) / logStep)); + }; + + auto lastValue = quantize(lambda(events[0].value)); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto nextValue = quantize(lambda(events[i].value)); + const auto difference = nextValue > lastValue ? nextValue / lastValue : lastValue / nextValue; + + if (difference < step) { + fill(envelope.subspan(lastDelay, length), lastValue); + lastValue = nextValue; + lastDelay += length; + continue; + } + + const auto numSteps = std::round(std::log(difference) / logStep); + const auto stepLength = static_cast(length / numSteps); + for (int i = 0; i < static_cast(numSteps); ++i) { + fill(envelope.subspan(lastDelay, stepLength), lastValue); + lastValue = nextValue > lastValue ? lastValue * step : lastValue / step; + lastDelay += stepLength; + } + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void pitchBendEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) +{ + multiplicativeEnvelope(events, envelope, std::forward(lambda), step); +} + +template +void pitchBendEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) +{ + multiplicativeEnvelope(events, envelope, std::forward(lambda)); +} + +template +void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) +{ + const auto events = resources.midiState.getCCEvents(ccData.cc); + const auto curve = resources.curves.getCurve(ccData.data.curve); + if (ccData.data.step == 0.0f) { + linearEnvelope(events, span, [&ccData, &curve, &lambda](float x) { + return lambda(curve.evalNormalized(x) * ccData.data.value); + }); + } else { + const float stepSize { lambda(ccData.data.step) }; + linearEnvelope( + events, span, [&ccData, &curve, &lambda](float x) { + return lambda(curve.evalNormalized(x) * ccData.data.value); + }, + stepSize); + } +} + +template +void multiplicativeModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) +{ + const auto events = resources.midiState.getCCEvents(ccData.cc); + const auto curve = resources.curves.getCurve(ccData.data.curve); + if (ccData.data.step == 0.0f) { + multiplicativeEnvelope(events, span, [&ccData, &curve, &lambda](float x) { + return lambda(curve.evalNormalized(x) * ccData.data.value); + }); + } else { + const float stepSize { lambda(ccData.data.step) }; + multiplicativeEnvelope( + events, span, [&ccData, &curve, &lambda](float x) { + return lambda(curve.evalNormalized(x) * ccData.data.value); + }, + stepSize); + } +} + +inline void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData) +{ + linearModifier(resources, span, ccData, [](float x) { return x; }); +} +} diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index e4ff6c26..492cfb1e 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -185,7 +185,7 @@ inline void setRangeStartFromOpcode(const Opcode& opcode, Range& targ * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange) +inline void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange) { auto value = readOpcode(opcode.value, validRange); if (value && Default::ccNumberRange.containsWithEnd(opcode.parameters.back())) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 234878e9..3411e915 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -5,13 +5,12 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "Region.h" -#include "Defaults.h" #include "MathHelpers.h" #include "Macros.h" #include "Debug.h" #include "Opcode.h" #include "StringViewHelpers.h" -#include "MidiState.h" +#include "ModifierHelpers.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_cat.h" #include "absl/algorithm/container.h" @@ -297,55 +296,146 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) case hash("volume"): setValueFromOpcode(opcode, volume, Default::volumeRange); break; + case hash("volume_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + volumeCC[opcode.parameters.back()].curve = *value; + break; + case hash("volume_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeStepRange)) + volumeCC[opcode.parameters.back()].step = *value; + break; + case hash("volume_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + volumeCC[opcode.parameters.back()].smooth = *value; + break; case hash("gain_cc&"): case hash("gain_oncc&"): // fallthrough case hash("volume_oncc&"): if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) - volumeCC[opcode.parameters.back()] = *value; + volumeCC[opcode.parameters.back()].value = *value; break; case hash("amplitude"): if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) amplitude = normalizePercents(*value); break; + case hash("amplitude_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + amplitudeCC[opcode.parameters.back()].curve = *value; + break; + case hash("amplitude_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + amplitudeCC[opcode.parameters.back()].step = normalizePercents(*value); + break; + case hash("amplitude_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + amplitudeCC[opcode.parameters.back()].smooth = *value; + break; case hash("amplitude_cc&"): // fallthrough case hash("amplitude_oncc&"): if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) - amplitudeCC[opcode.parameters.back()] = normalizePercents(*value); + amplitudeCC[opcode.parameters.back()].value = normalizePercents(*value); break; case hash("pan"): if (auto value = readOpcode(opcode.value, Default::panRange)) pan = normalizePercents(*value); break; + case hash("pan_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + panCC[opcode.parameters.back()].curve = *value; + break; + case hash("pan_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::panStepRange)) + panCC[opcode.parameters.back()].step = normalizePercents(*value); + break; + case hash("pan_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + panCC[opcode.parameters.back()].smooth = *value; + break; case hash("pan_cc&"): case hash("pan_oncc&"): if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::panCCRange)) - panCC[opcode.parameters.back()] = normalizePercents(*value); + panCC[opcode.parameters.back()].value = normalizePercents(*value); break; case hash("position"): if (auto value = readOpcode(opcode.value, Default::positionRange)) position = normalizePercents(*value); break; + case hash("position_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + positionCC[opcode.parameters.back()].curve = *value; + break; + case hash("position_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::positionStepRange)) + positionCC[opcode.parameters.back()].step = normalizePercents(*value); + break; + case hash("position_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + positionCC[opcode.parameters.back()].smooth = *value; + break; + case hash("position_cc&"): // fallthrough case hash("position_oncc&"): if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::positionCCRange)) - positionCC[opcode.parameters.back()] = normalizePercents(*value); + positionCC[opcode.parameters.back()].value = normalizePercents(*value); break; case hash("width"): if (auto value = readOpcode(opcode.value, Default::widthRange)) width = normalizePercents(*value); break; + case hash("width_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + widthCC[opcode.parameters.back()].curve = *value; + break; + case hash("width_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::widthStepRange)) + widthCC[opcode.parameters.back()].step = normalizePercents(*value); + break; + case hash("width_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + widthCC[opcode.parameters.back()].smooth = *value; + break; case hash("width_oncc&"): if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::widthCCRange)) - widthCC[opcode.parameters.back()] = normalizePercents(*value); + widthCC[opcode.parameters.back()].value = normalizePercents(*value); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -726,6 +816,27 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) case hash("pitch"): setValueFromOpcode(opcode, tune, Default::tuneRange); break; + case hash("pitch_curvecc&"): // fallthrough + case hash("tune_curvecc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::curveCCRange)) + tuneCC[opcode.parameters.back()].curve = *value; + break; + case hash("pitch_stepcc&"): // fallthrough + case hash("tune_stepcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneStepRange)) + tuneCC[opcode.parameters.back()].step = *value; + break; + case hash("pitch_smoothcc&"): // fallthrough + case hash("tune_smoothcc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::smoothCCRange)) + tuneCC[opcode.parameters.back()].smooth = *value; + break; case hash("tune_cc&"): case hash("tune_oncc&"): case hash("pitch_cc&"): @@ -733,7 +844,7 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) if (opcode.parameters.back() > config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) - tuneCC[opcode.parameters.back()] = *value; + tuneCC[opcode.parameters.back()].value = *value; break; case hash("bend_up"): setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -1078,15 +1189,15 @@ float sfz::Region::getCrossfadeGain() const noexcept float gain { 1.0f }; // Crossfades due to CC states - for (const auto& valuePair : crossfadeCCInRange) { - const auto ccValue = midiState.getCCValue(valuePair.cc); - const auto crossfadeRange = valuePair.value; + for (const auto& ccData : crossfadeCCInRange) { + const auto ccValue = midiState.getCCValue(ccData.cc); + const auto crossfadeRange = ccData.data; gain *= crossfadeIn(crossfadeRange, ccValue, crossfadeCCCurve); } - for (const auto& valuePair : crossfadeCCOutRange) { - const auto ccValue = midiState.getCCValue(valuePair.cc); - const auto crossfadeRange = valuePair.value; + for (const auto& ccData : crossfadeCCOutRange) { + const auto ccValue = midiState.getCCValue(ccData.cc); + const auto crossfadeRange = ccData.data; gain *= crossfadeOut(crossfadeRange, ccValue, crossfadeCCCurve); } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 73f3bc53..fa952675 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -14,10 +14,8 @@ #include "Opcode.h" #include "AudioBuffer.h" #include "MidiState.h" -#include "absl/strings/str_cat.h" +#include "absl/types/optional.h" #include -#include -#include #include #include @@ -285,11 +283,11 @@ struct Region { float pan { normalizePercents(Default::pan) }; // pan float width { normalizePercents(Default::width) }; // width float position { normalizePercents(Default::position) }; // position - CCMap volumeCC { Default::zeroModifier }; // volume_oncc - CCMap amplitudeCC { Default::zeroModifier }; // amplitude_oncc - CCMap panCC { Default::zeroModifier }; // pan_oncc - CCMap widthCC { Default::zeroModifier }; // width_oncc - CCMap positionCC { Default::zeroModifier }; // position_oncc + CCMap volumeCC { {} }; // volume_oncc + CCMap amplitudeCC { {} }; // amplitude_oncc + CCMap panCC { {} }; // pan_oncc + CCMap widthCC { {} }; // width_oncc + CCMap positionCC { {} }; // position_oncc uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter float ampKeytrack { Default::ampKeytrack }; // amp_keytrack float ampVeltrack { Default::ampVeltrack }; // amp_keytrack @@ -317,7 +315,7 @@ struct Region { int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack int transpose { Default::transpose }; // transpose int tune { Default::tune }; // tune - CCMap tuneCC { Default::tune }; + CCMap tuneCC { {} }; int bendUp { Default::bendUp }; int bendDown { Default::bendDown }; int bendStep { Default::bendStep }; diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 1367d714..17f924a9 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -11,6 +11,7 @@ #include "EQPool.h" #include "Logger.h" #include "Wavetables.h" +#include "Curve.h" namespace sfz { @@ -21,6 +22,7 @@ struct Resources BufferPool bufferPool; MidiState midiState; Logger logger; + CurveSet curves; FilePool filePool { logger }; FilterPool filterPool { midiState }; EQPool eqPool { midiState }; @@ -38,5 +40,14 @@ struct Resources bufferPool.setBufferSize(samplesPerBlock); midiState.setSamplesPerBlock(samplesPerBlock); } + + void clear() + { + curves = CurveSet::createPredefined(); + filePool.clear(); + wavePool.clearFileWaves(); + logger.clear(); + midiState.reset(); + } }; } diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 64d6ed6c..60979282 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -24,44 +24,35 @@ using CCNamePair = std::pair; template using MidiNoteArray = std::array; template -struct CCValuePair { +struct CCData { int cc; - ValueType value; + ValueType data; + static_assert(config::numCCs - 1 < std::numeric_limits::max(), "The cc type in the CCData struct cannot support the required number of CCs"); }; -template -struct CCValuePairComparator { - bool operator()(const CCValuePair& valuePair, const int& cc) - { - return (valuePair.cc < cc); - } - - bool operator()(const int& cc, const CCValuePair& valuePair) - { - return (cc < valuePair.cc); - } - - bool operator()(const CCValuePair& lhs, const CCValuePair& rhs) - { - return (lhs.cc < rhs.cc); - } +struct Modifier { + float value { 0.0f }; + float step { 0.0f }; + uint8_t curve { 0 }; + uint8_t smooth { 0 }; + static_assert(config::maxCurves - 1 <= std::numeric_limits::max(), "The curve type in the Modifier struct cannot support the required number of curves"); }; template -struct CCValuePairComparator { - bool operator()(const CCValuePair& valuePair, const ValueType& value) +struct CCDataComparator { + bool operator()(const CCData& ccData, const int& cc) { - return (valuePair.value < value); + return (ccData.cc < cc); } - bool operator()(const ValueType& value, const CCValuePair& valuePair) + bool operator()(const int& cc, const CCData& ccData) { - return (value < valuePair.value); + return (cc < ccData.cc); } - bool operator()(const CCValuePair& lhs, const CCValuePair& rhs) + bool operator()(const CCData& lhs, const CCData& rhs) { - return (lhs.value < rhs.value); + return (lhs.cc < rhs.cc); } }; @@ -273,200 +264,5 @@ bool findDefine(absl::string_view line, absl::string_view& variable, absl::strin */ bool findInclude(absl::string_view line, std::string& path); -/** - * @brief multiply a value by a factor, in cents. To be used for pitch variations. - * - * @param base - * @param modifier - */ -inline CXX14_CONSTEXPR float multiplyByCentsModifier(int modifier, float base) -{ - return base * centsFactor(modifier); -} - -template -inline CXX14_CONSTEXPR float gainModifier(T modifier, float value) -{ - return value * modifier; -} - -/** - * @brief Compute a crossfade in value with respect to a crossfade range (note, velocity, cc, ...) - */ -template -float crossfadeIn(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) -{ - if (value < crossfadeRange.getStart()) - return 0.0f; - - const auto length = static_cast(crossfadeRange.length()); - if (length == 0.0f) - return 1.0f; - - else if (value < crossfadeRange.getEnd()) { - const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; - if (curve == SfzCrossfadeCurve::power) - return sqrt(crossfadePosition); - if (curve == SfzCrossfadeCurve::gain) - return crossfadePosition; - } - - return 1.0f; -} - -/** - * @brief Compute a crossfade out value with respect to a crossfade range (note, velocity, cc, ...) - */ -template -float crossfadeOut(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) -{ - if (value > crossfadeRange.getEnd()) - return 0.0f; - - const auto length = static_cast(crossfadeRange.length()); - if (length == 0.0f) - return 1.0f; - - else if (value > crossfadeRange.getStart()) { - const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; - if (curve == SfzCrossfadeCurve::power) - return std::sqrt(1 - crossfadePosition); - if (curve == SfzCrossfadeCurve::gain) - return 1 - crossfadePosition; - } - - return 1.0f; -} - -template -void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) -{ - ASSERT(events.size() > 0); - ASSERT(events[0].delay == 0); - if (envelope.size() == 0) - return; - - const auto maxDelay = static_cast(envelope.size() - 1); - - auto lastValue = lambda(events[0].value); - auto lastDelay = events[0].delay; - for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { - const auto length = min(events[i].delay, maxDelay) - lastDelay; - const auto step = (lambda(events[i].value) - lastValue) / length; - lastValue = linearRamp(envelope.subspan(lastDelay, length), lastValue, step); - lastDelay += length; - } - fill(envelope.subspan(lastDelay), lastValue); -} - -template -void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) -{ - ASSERT(events.size() > 0); - ASSERT(events[0].delay == 0); - ASSERT(step != 0.0); - - if (envelope.size() == 0) - return; - - auto quantize = [step](float value) -> float { - return std::round(value / step) * step; - }; - const auto maxDelay = static_cast(envelope.size() - 1); - - auto lastValue = quantize(lambda(events[0].value)); - auto lastDelay = events[0].delay; - for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { - const auto nextValue = quantize(lambda(events[i].value)); - const auto difference = std::abs(nextValue - lastValue); - const auto length = min(events[i].delay, maxDelay) - lastDelay; - - if (difference < step) { - fill(envelope.subspan(lastDelay, length), lastValue); - lastValue = nextValue; - lastDelay += length; - continue; - } - - const auto numSteps = static_cast(difference / step); - const auto stepLength = static_cast(length / numSteps); - for (int i = 0; i < numSteps; ++i) { - fill(envelope.subspan(lastDelay, stepLength), lastValue); - lastValue += lastValue <= nextValue ? step : -step; - lastDelay += stepLength; - } - } - fill(envelope.subspan(lastDelay), lastValue); -} - -template -void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) -{ - ASSERT(events.size() > 0); - ASSERT(events[0].delay == 0); - - if (envelope.size() == 0) - return; - const auto maxDelay = static_cast(envelope.size() - 1); - - auto lastValue = lambda(events[0].value); - auto lastDelay = events[0].delay; - for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { - const auto length = min(events[i].delay, maxDelay) - lastDelay; - const auto nextValue = lambda(events[i].value); - const auto step = std::exp((std::log(nextValue) - std::log(lastValue)) / length); - multiplicativeRamp(envelope.subspan(lastDelay, length), lastValue, step); - lastValue = nextValue; - lastDelay += length; - } - fill(envelope.subspan(lastDelay), lastValue); -} - -template -void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) -{ - ASSERT(events.size() > 0); - ASSERT(events[0].delay == 0); - ASSERT(step != 0.0f); - - if (envelope.size() == 0) - return; - const auto maxDelay = static_cast(envelope.size() - 1); - - const auto logStep = std::log(step); - // If we assume that a = b.q^r for b in (1, q) then - // log a log b - // ----- = ----- + r - // log q log q - // and log(b)\log(q) is between 0 and 1. - auto quantize = [logStep](float value) -> float { - return std::exp(logStep * std::round(std::log(value) / logStep)); - }; - - auto lastValue = quantize(lambda(events[0].value)); - auto lastDelay = events[0].delay; - for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { - const auto length = min(events[i].delay, maxDelay) - lastDelay; - const auto nextValue = quantize(lambda(events[i].value)); - const auto difference = nextValue > lastValue ? nextValue / lastValue : lastValue / nextValue; - - if (difference < step) { - fill(envelope.subspan(lastDelay, length), lastValue); - lastValue = nextValue; - lastDelay += length; - continue; - } - - const auto numSteps = static_cast(std::log(difference) / logStep); - const auto stepLength = static_cast(length / numSteps); - for (int i = 0; i < numSteps; ++i) { - fill(envelope.subspan(lastDelay, stepLength), lastValue); - lastValue = nextValue > lastValue ? lastValue * step : lastValue / step; - lastDelay += stepLength; - } - } - fill(envelope.subspan(lastDelay), lastValue); -} - } // namespace sfz diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0870905f..19a3ba5b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -71,7 +71,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorsetGainToMain(1.0); effectBuses[0]->setSamplesPerBlock(samplesPerBlock); effectBuses[0]->setSampleRate(sampleRate); - curves = CurveSet::createPredefined(); - resources.filePool.clear(); - resources.wavePool.clearFileWaves(); - resources.logger.clear(); + resources.clear(); numGroups = 0; numMasters = 0; - fileTicket = -1; defaultSwitch = absl::nullopt; defaultPath = ""; - resources.midiState.reset(); ccNames.clear(); globalOpcodes.clear(); masterOpcodes.clear(); @@ -814,7 +810,7 @@ int sfz::Synth::getNumMasters() const noexcept } int sfz::Synth::getNumCurves() const noexcept { - return static_cast(curves.getNumCurves()); + return static_cast(resources.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 6b3d4c37..4d34d8e8 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -10,7 +10,6 @@ #include "Voice.h" #include "Region.h" #include "Effects.h" -#include "Curve.h" #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" @@ -340,7 +339,7 @@ public: */ void disableFreeWheeling() noexcept; - const MidiState& getMidiState() const noexcept { return resources.midiState; } + const Resources& getResources() const noexcept { return resources; } /** * @brief Check if the SFZ should be reloaded. @@ -515,9 +514,6 @@ private: typedef std::unique_ptr EffectBusPtr; std::vector effectBuses; // 0 is "main", 1-N are "fx1"-"fxN" - // Curves - CurveSet curves; - int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; float volume { Default::globalVolume }; @@ -526,7 +522,6 @@ private: // Distribution used to generate random value for the *rand opcodes std::uniform_real_distribution randNoteDistribution { 0, 1 }; - unsigned fileTicket { 1 }; std::mutex callbackGuard; bool freeWheeling { false }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index fcb1d9fd..cfa8bfbe 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -4,16 +4,14 @@ // 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 "Macros.h" #include "Voice.h" -#include "AudioSpan.h" -#include "Config.h" +#include "Macros.h" #include "Defaults.h" +#include "ModifierHelpers.h" #include "MathHelpers.h" #include "SIMDHelpers.h" #include "SfzHelpers.h" #include "absl/algorithm/container.h" -#include sfz::Voice::Voice(sfz::Resources& resources) : resources(resources) @@ -264,28 +262,32 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept // Amplitude envelope applyGain(baseGain, modulationSpan); for (const auto& mod : region->amplitudeCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + linearModifier(resources, *tempSpan, mod); applyGain(*tempSpan, modulationSpan); } // Crossfade envelopes for (const auto& mod : region->crossfadeCCInRange) { const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeIn(mod.value, x, xfCurve); }); + linearEnvelope(events, *tempSpan, [&](float x) { + return crossfadeIn(mod.data, x, xfCurve); + }); applyGain(*tempSpan, modulationSpan); } for (const auto& mod : region->crossfadeCCOutRange) { const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeOut(mod.value, x, xfCurve); }); + linearEnvelope(events, *tempSpan, [&](float x) { + return crossfadeOut(mod.data, x, xfCurve); + }); applyGain(*tempSpan, modulationSpan); } // Volume envelope applyGain(db2mag(baseVolumedB), modulationSpan); for (const auto& mod : region->volumeCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - multiplicativeEnvelope(events, *tempSpan, [&](float x) { return db2mag(x * mod.value); }); + multiplicativeModifier(resources, *tempSpan, mod, [](float x) { + return db2mag(x); + }); applyGain(*tempSpan, modulationSpan); } } @@ -337,8 +339,7 @@ void sfz::Voice::panStageMono(AudioSpan buffer) noexcept // Apply panning fill(*modulationSpan, region->pan); for (const auto& mod : region->panCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + linearModifier(resources, *tempSpan, mod); add(*tempSpan, *modulationSpan); } pan(*modulationSpan, leftBuffer, rightBuffer); @@ -357,30 +358,24 @@ void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept return; // Apply panning - // panningModulation(*modulationSpan); fill(*modulationSpan, region->pan); for (const auto& mod : region->panCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + linearModifier(resources, *tempSpan, mod); add(*tempSpan, *modulationSpan); } pan(*modulationSpan, leftBuffer, rightBuffer); // Apply the width/position process - // widthModulation(*modulationSpan); fill(*modulationSpan, region->width); for (const auto& mod : region->widthCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + linearModifier(resources, *tempSpan, mod); add(*tempSpan, *modulationSpan); } width(*modulationSpan, leftBuffer, rightBuffer); - // positionModulation(*modulationSpan); fill(*modulationSpan, region->position); for (const auto& mod : region->positionCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + linearModifier(resources, *tempSpan, mod); add(*tempSpan, *modulationSpan); } pan(*modulationSpan, leftBuffer, rightBuffer); @@ -451,14 +446,13 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept }; if (region->bendStep > 1) - multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor); + pitchBendEnvelope(events, *bends, bendLambda, bendStepFactor); else - multiplicativeEnvelope(events, *bends, bendLambda); + pitchBendEnvelope(events, *bends, bendLambda); applyGain(*bends, *jumps); for (const auto& mod : region->tuneCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); }); + multiplicativeModifier(resources, *bends, mod, [](float x) { return centsFactor(x); }); applyGain(*bends, *jumps); } @@ -555,14 +549,15 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept return centsFactor(bendInCents); }; if (region->bendStep > 1) - multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor); + pitchBendEnvelope(events, *bends, bendLambda, bendStepFactor); else - multiplicativeEnvelope(events, *bends, bendLambda); + pitchBendEnvelope(events, *bends, bendLambda); applyGain(*bends, *frequencies); for (const auto& mod : region->tuneCC) { - const auto events = resources.midiState.getCCEvents(mod.cc); - multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); }); + multiplicativeModifier(resources, *bends, mod, [](float x) { + return centsFactor(x); + }); applyGain(*bends, *frequencies); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 1723c416..b4e61c5e 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -10,13 +10,10 @@ #include "HistoricalBuffer.h" #include "Region.h" #include "AudioBuffer.h" -#include "MidiState.h" -#include "Wavetables.h" #include "Resources.h" #include "AudioSpan.h" #include "LeakDetector.h" -#include -#include +#include "absl/types/span.h" #include #include diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 9c8b054b..4c3c9f9d 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -33,7 +33,7 @@ TEST_CASE("[Curve] Bipolar -1 to 1") 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) ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(-0.4).margin(1e-3) ); } TEST_CASE("[Curve] Bipolar 1 to 0") @@ -59,7 +59,7 @@ TEST_CASE("[Curve] Bipolar 1 to -1") 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) ); + REQUIRE( curve.evalNormalized(0.3f) == Approx(0.4).margin(1e-3) ); } TEST_CASE("[Curve] x**2") @@ -199,6 +199,17 @@ TEST_CASE("[Curve] Add curves to CurveSet") REQUIRE( curveSet.getCurve(4).evalCC7(0) == 1.0f ); } +TEST_CASE("[Curve] Add bad indices") +{ + sfz::CurveSet curveSet; + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0), -2); + REQUIRE( curveSet.getNumCurves() == 0 ); + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0), 256); + REQUIRE( curveSet.getNumCurves() == 0 ); + curveSet.addCurve(sfz::Curve::buildPredefinedCurve(0), 512); + REQUIRE( curveSet.getNumCurves() == 0 ); +} + TEST_CASE("[Curve] Default CurveSet") { auto curveSet = sfz::CurveSet::createPredefined(); @@ -209,7 +220,7 @@ TEST_CASE("[Curve] Default CurveSet") 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(1).evalNormalized(0.3f) == Approx(-0.4).margin(1e-3) ); REQUIRE( curveSet.getCurve(2).evalNormalized(0.0f) == 1.0f ); REQUIRE( curveSet.getCurve(2).evalNormalized(1.0f) == 0.0f ); @@ -217,7 +228,7 @@ TEST_CASE("[Curve] Default CurveSet") 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(3).evalNormalized(0.3f) == Approx(0.4).margin(1e-3) ); REQUIRE( curveSet.getCurve(4).evalNormalized(0.0f) == 0.0f ); REQUIRE( curveSet.getCurve(4).evalNormalized(1.0f) == 1.0f ); diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 545ea594..64bcb7e4 100644 --- a/tests/EventEnvelopesT.cpp +++ b/tests/EventEnvelopesT.cpp @@ -4,7 +4,7 @@ // 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/SfzHelpers.h" +#include "sfizz/ModifierHelpers.h" #include "sfizz/Buffer.h" #include "catch2/catch.hpp" #include @@ -42,13 +42,13 @@ TEST_CASE("[Envelopes] Empty") std::array expected { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; std::array expectedMul { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); multiplicativeEnvelope(events, absl::MakeSpan(output), expModifier); - REQUIRE(output == expectedMul); + REQUIRE(approxEqual(output, expectedMul)); multiplicativeEnvelope(events, absl::MakeSpan(output), expModifier, 2.0f); - REQUIRE(output == expectedMul); + REQUIRE(approxEqual(output, expectedMul)); } TEST_CASE("[Envelopes] Linear basic") @@ -60,7 +60,7 @@ TEST_CASE("[Envelopes] Linear basic") std::array output; std::array expected { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] 2 events, close") @@ -73,7 +73,7 @@ TEST_CASE("[LinearEnvelope] 2 events, close") std::array output; std::array expected { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] 2 events, far") @@ -86,7 +86,7 @@ TEST_CASE("[LinearEnvelope] 2 events, far") std::array output; std::array expected { 0.0f, 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.0f, 2.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] 3 events, out of block") @@ -100,7 +100,7 @@ TEST_CASE("[LinearEnvelope] 3 events, out of block") std::array output; std::array expected { 0.0f, 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] 2 events, function") @@ -113,7 +113,7 @@ TEST_CASE("[LinearEnvelope] 2 events, function") std::array output; std::array expected { 0.0f, 1.0f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.0f, 4.0f }; linearEnvelope(events, absl::MakeSpan(output), twiceModifier); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] Get quantized") @@ -126,7 +126,7 @@ TEST_CASE("[LinearEnvelope] Get quantized") std::array output; std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets") @@ -137,9 +137,9 @@ TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets") { 6, 1.9f } }; std::array output; - std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; + std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] Get quantized with 2 steps") @@ -152,7 +152,7 @@ TEST_CASE("[LinearEnvelope] Get quantized with 2 steps") std::array output; std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of block step") @@ -166,7 +166,7 @@ TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of std::array output; std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } @@ -180,7 +180,7 @@ TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps") std::array output; std::array expected { 3.0f, 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] Basic event") @@ -224,52 +224,140 @@ TEST_CASE("[MultiplicativeEnvelope] 2 events, far") TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps") { sfz::EventVector events { - { 0, 1.0f }, - { 2, 2.0f }, - { 6, 4.0f } + { 0, 1.3f }, + { 2, 2.1f }, + { 6, 4.2f } }; std::array output; std::array expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f }; multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] Get quantized with an unquantized out of range step") { sfz::EventVector events { - { 0, 1.0f }, - { 2, 2.0f }, - { 6, 4.0f }, + { 0, 1.3f }, + { 2, 2.1f }, + { 6, 4.1f }, { 10, 8.2f } }; std::array output; std::array expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f }; multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps") { sfz::EventVector events { - { 0, 4.0f }, - { 2, 2.0f }, - { 6, 0.5f } + { 0, 4.1f }, + { 2, 2.2f }, + { 6, 0.4f } }; std::array output; std::array expected { 4.0f, 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f }; multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); - REQUIRE(output == expected); + REQUIRE(approxEqual(output, expected)); } -TEST_CASE("[MultiplicativeEnvelope] Get quantized with unclean events") +TEST_CASE("[linearModifiers] Compare with envelopes") { - sfz::EventVector events { - { 0, 1.0f }, - { 2, 1.2f }, - { 6, 2.5f } - }; - std::array output; - std::array expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; - multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); - REQUIRE(output == expected); + sfz::Resources resources; + resources.curves = sfz::CurveSet::createPredefined(); + + sfz::CCData ccData; + ccData.cc = 20; + ccData.data.value = 100.0f; + + resources.midiState.ccEvent(5, 20, 0.1); + resources.midiState.ccEvent(10, 20, 0.8); + + std::array output; + std::array envelope; + + linearEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return ccData.data.value * x; + }); + linearModifier(resources, absl::MakeSpan(output), ccData); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 1; + linearEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return ccData.data.value * (2 * x - 1); + }); + linearModifier(resources, absl::MakeSpan(output), ccData); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 3; + ccData.data.value = 10.0f; + linearEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return ccData.data.value * (1 - 2 * x); + }); + linearModifier(resources, absl::MakeSpan(output), ccData); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 2; + ccData.data.value = 20.0f; + ccData.data.step = 2.5f; + linearEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return ccData.data.value * (1 - x); + }, ccData.data.step); + linearModifier(resources, absl::MakeSpan(output), ccData); + REQUIRE(approxEqual(output, envelope)); } + +TEST_CASE("[multiplicativeModifiers] Compare with envelopes") +{ + sfz::Resources resources; + resources.curves = sfz::CurveSet::createPredefined(); + + sfz::CCData ccData; + ccData.cc = 20; + ccData.data.value = 100.0f; + + resources.midiState.ccEvent(5, 20, 0.1); + resources.midiState.ccEvent(15, 20, 0.8); + + std::array output; + std::array envelope; + + multiplicativeEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return db2mag(ccData.data.value * x); + }); + multiplicativeModifier(resources, absl::MakeSpan(output), ccData, [](float x) { + return db2mag(x); + }); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 1; + multiplicativeEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return db2mag(ccData.data.value * (2 * x - 1)); + }); + multiplicativeModifier(resources, absl::MakeSpan(output), ccData, [](float x) { + return db2mag(x); + }); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 3; + ccData.data.value = 10.0f; + multiplicativeEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return db2mag(ccData.data.value * (1 - 2 * x)); + }); + multiplicativeModifier(resources, absl::MakeSpan(output), ccData, [](float x) { + return db2mag(x); + }); + REQUIRE(approxEqual(output, envelope)); + + ccData.data.curve = 2; + ccData.data.value = 20.0f; + ccData.data.step = 2.5f; + multiplicativeEnvelope(resources.midiState.getCCEvents(20), absl::MakeSpan(envelope), [&ccData](float x) { + return db2mag(ccData.data.value * (1 - x)); + }, db2mag(ccData.data.step) ); + multiplicativeModifier(resources, absl::MakeSpan(output), ccData, [](float x) { + return db2mag(x); + }); + REQUIRE(approxEqual(output, envelope)); +} + diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 2442d8df..4dd43c8f 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -351,7 +351,7 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines") REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); REQUIRE(!synth.getRegionView(2)->amplitudeCC.empty()); REQUIRE(synth.getRegionView(2)->amplitudeCC.contains(10)); - REQUIRE(synth.getRegionView(2)->amplitudeCC.getWithDefault(10) == 0.34f); + REQUIRE(synth.getRegionView(2)->amplitudeCC.getWithDefault(10).value == 0.34f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") @@ -394,9 +394,10 @@ TEST_CASE("[Files] Default path is ignored for generators") TEST_CASE("[Files] Set CC applies properly") { sfz::Synth synth; + const auto& midiState = synth.getResources().midiState; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/set_cc.sfz"); - REQUIRE(synth.getMidiState().getCCValue(142) == 63_norm); - REQUIRE(synth.getMidiState().getCCValue(61) == 122_norm); + REQUIRE(midiState.getCCValue(142) == 63_norm); + REQUIRE(midiState.getCCValue(61) == 122_norm); } TEST_CASE("[Files] Note and octave offsets") diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 86fde09c..f982c424 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -470,7 +470,25 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.panCC.empty()); region.parseOpcode({ "pan_oncc45", "4.2" }); REQUIRE(region.panCC.contains(45)); - REQUIRE(region.panCC[45] == 0.042_a); + REQUIRE(region.panCC[45].value == 0.042_a); + region.parseOpcode({ "pan_curvecc17", "18" }); + REQUIRE(region.panCC[17].curve == 18); + region.parseOpcode({ "pan_curvecc17", "15482" }); + REQUIRE(region.panCC[17].curve == 255); + region.parseOpcode({ "pan_curvecc17", "-2" }); + REQUIRE(region.panCC[17].curve == 0); + region.parseOpcode({ "pan_smoothcc14", "85" }); + REQUIRE(region.panCC[14].smooth == 85); + region.parseOpcode({ "pan_smoothcc14", "15482" }); + REQUIRE(region.panCC[14].smooth == 127); + region.parseOpcode({ "pan_smoothcc14", "-2" }); + REQUIRE(region.panCC[14].smooth == 0); + region.parseOpcode({ "pan_stepcc120", "24" }); + REQUIRE(region.panCC[120].step == 0.24_a); + region.parseOpcode({ "pan_stepcc120", "15482" }); + REQUIRE(region.panCC[120].step == 2.0_a); + region.parseOpcode({ "pan_stepcc120", "-2" }); + REQUIRE(region.panCC[120].step == 0.0f); } SECTION("width") @@ -491,7 +509,25 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.widthCC.empty()); region.parseOpcode({ "width_oncc45", "4.2" }); REQUIRE(region.widthCC.contains(45)); - REQUIRE(region.widthCC[45] == 0.042_a); + REQUIRE(region.widthCC[45].value == 0.042_a); + region.parseOpcode({ "width_curvecc17", "18" }); + REQUIRE(region.widthCC[17].curve == 18); + region.parseOpcode({ "width_curvecc17", "15482" }); + REQUIRE(region.widthCC[17].curve == 255); + region.parseOpcode({ "width_curvecc17", "-2" }); + REQUIRE(region.widthCC[17].curve == 0); + region.parseOpcode({ "width_smoothcc14", "85" }); + REQUIRE(region.widthCC[14].smooth == 85); + region.parseOpcode({ "width_smoothcc14", "15482" }); + REQUIRE(region.widthCC[14].smooth == 127); + region.parseOpcode({ "width_smoothcc14", "-2" }); + REQUIRE(region.widthCC[14].smooth == 0); + region.parseOpcode({ "width_stepcc120", "24" }); + REQUIRE(region.widthCC[120].step == 0.24_a); + region.parseOpcode({ "width_stepcc120", "15482" }); + REQUIRE(region.widthCC[120].step == 2.0_a); + region.parseOpcode({ "width_stepcc120", "-20" }); + REQUIRE(region.widthCC[120].step == 0.0f); } SECTION("position") @@ -512,7 +548,25 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.positionCC.empty()); region.parseOpcode({ "position_oncc45", "4.2" }); REQUIRE(region.positionCC.contains(45)); - REQUIRE(region.positionCC[45] == 0.042_a); + REQUIRE(region.positionCC[45].value == 0.042_a); + region.parseOpcode({ "position_curvecc17", "18" }); + REQUIRE(region.positionCC[17].curve == 18); + region.parseOpcode({ "position_curvecc17", "15482" }); + REQUIRE(region.positionCC[17].curve == 255); + region.parseOpcode({ "position_curvecc17", "-2" }); + REQUIRE(region.positionCC[17].curve == 0); + region.parseOpcode({ "position_smoothcc14", "85" }); + REQUIRE(region.positionCC[14].smooth == 85); + region.parseOpcode({ "position_smoothcc14", "15482" }); + REQUIRE(region.positionCC[14].smooth == 127); + region.parseOpcode({ "position_smoothcc14", "-2" }); + REQUIRE(region.positionCC[14].smooth == 0); + region.parseOpcode({ "position_stepcc120", "24" }); + REQUIRE(region.positionCC[120].step == 0.24_a); + region.parseOpcode({ "position_stepcc120", "15482" }); + REQUIRE(region.positionCC[120].step == 2.0_a); + region.parseOpcode({ "position_stepcc120", "-2" }); + REQUIRE(region.positionCC[120].step == 0.0f); } SECTION("amp_keycenter") @@ -974,13 +1028,13 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.amplitudeEG.ccRelease->cc == 5); REQUIRE(region.amplitudeEG.ccStart->cc == 6); REQUIRE(region.amplitudeEG.ccSustain->cc == 7); - REQUIRE(region.amplitudeEG.ccAttack->value == 1.0f); - REQUIRE(region.amplitudeEG.ccDecay->value == 2.0f); - REQUIRE(region.amplitudeEG.ccDelay->value == 3.0f); - REQUIRE(region.amplitudeEG.ccHold->value == 4.0f); - REQUIRE(region.amplitudeEG.ccRelease->value == 5.0f); - REQUIRE(region.amplitudeEG.ccStart->value == 6.0f); - REQUIRE(region.amplitudeEG.ccSustain->value == 7.0f); + REQUIRE(region.amplitudeEG.ccAttack->data == 1.0f); + REQUIRE(region.amplitudeEG.ccDecay->data == 2.0f); + REQUIRE(region.amplitudeEG.ccDelay->data == 3.0f); + REQUIRE(region.amplitudeEG.ccHold->data == 4.0f); + REQUIRE(region.amplitudeEG.ccRelease->data == 5.0f); + REQUIRE(region.amplitudeEG.ccStart->data == 6.0f); + REQUIRE(region.amplitudeEG.ccSustain->data == 7.0f); // region.parseOpcode({ "ampeg_attack_oncc1", "101" }); region.parseOpcode({ "ampeg_decay_oncc2", "101" }); @@ -989,13 +1043,13 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "ampeg_release_oncc5", "101" }); region.parseOpcode({ "ampeg_start_oncc6", "101" }); region.parseOpcode({ "ampeg_sustain_oncc7", "101" }); - REQUIRE(region.amplitudeEG.ccAttack->value == 100.0f); - REQUIRE(region.amplitudeEG.ccDecay->value == 100.0f); - REQUIRE(region.amplitudeEG.ccDelay->value == 100.0f); - REQUIRE(region.amplitudeEG.ccHold->value == 100.0f); - REQUIRE(region.amplitudeEG.ccRelease->value == 100.0f); - REQUIRE(region.amplitudeEG.ccStart->value == 100.0f); - REQUIRE(region.amplitudeEG.ccSustain->value == 100.0f); + REQUIRE(region.amplitudeEG.ccAttack->data == 100.0f); + REQUIRE(region.amplitudeEG.ccDecay->data == 100.0f); + REQUIRE(region.amplitudeEG.ccDelay->data == 100.0f); + REQUIRE(region.amplitudeEG.ccHold->data == 100.0f); + REQUIRE(region.amplitudeEG.ccRelease->data == 100.0f); + REQUIRE(region.amplitudeEG.ccStart->data == 100.0f); + REQUIRE(region.amplitudeEG.ccSustain->data == 100.0f); // region.parseOpcode({ "ampeg_attack_oncc1", "-101" }); region.parseOpcode({ "ampeg_decay_oncc2", "-101" }); @@ -1004,13 +1058,13 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "ampeg_release_oncc5", "-101" }); region.parseOpcode({ "ampeg_start_oncc6", "-101" }); region.parseOpcode({ "ampeg_sustain_oncc7", "-101" }); - REQUIRE(region.amplitudeEG.ccAttack->value == -100.0f); - REQUIRE(region.amplitudeEG.ccDecay->value == -100.0f); - REQUIRE(region.amplitudeEG.ccDelay->value == -100.0f); - REQUIRE(region.amplitudeEG.ccHold->value == -100.0f); - REQUIRE(region.amplitudeEG.ccRelease->value == -100.0f); - REQUIRE(region.amplitudeEG.ccStart->value == -100.0f); - REQUIRE(region.amplitudeEG.ccSustain->value == -100.0f); + REQUIRE(region.amplitudeEG.ccAttack->data == -100.0f); + REQUIRE(region.amplitudeEG.ccDecay->data == -100.0f); + REQUIRE(region.amplitudeEG.ccDelay->data == -100.0f); + REQUIRE(region.amplitudeEG.ccHold->data == -100.0f); + REQUIRE(region.amplitudeEG.ccRelease->data == -100.0f); + REQUIRE(region.amplitudeEG.ccStart->data == -100.0f); + REQUIRE(region.amplitudeEG.ccSustain->data == -100.0f); } SECTION("sustain_sw and sostenuto_sw") @@ -1448,10 +1502,28 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.amplitudeCC.empty()); region.parseOpcode({ "amplitude_cc1", "40" }); REQUIRE(region.amplitudeCC.contains(1)); - REQUIRE(region.amplitudeCC[1] == 0.40_a); + REQUIRE(region.amplitudeCC[1].value == 0.40_a); region.parseOpcode({ "amplitude_oncc2", "30" }); REQUIRE(region.amplitudeCC.contains(2)); - REQUIRE(region.amplitudeCC[2] == 0.30_a); + REQUIRE(region.amplitudeCC[2].value == 0.30_a); + region.parseOpcode({ "amplitude_curvecc17", "18" }); + REQUIRE(region.amplitudeCC[17].curve == 18); + region.parseOpcode({ "amplitude_curvecc17", "15482" }); + REQUIRE(region.amplitudeCC[17].curve == 255); + region.parseOpcode({ "amplitude_curvecc17", "-2" }); + REQUIRE(region.amplitudeCC[17].curve == 0); + region.parseOpcode({ "amplitude_smoothcc14", "85" }); + REQUIRE(region.amplitudeCC[14].smooth == 85); + region.parseOpcode({ "amplitude_smoothcc14", "15482" }); + REQUIRE(region.amplitudeCC[14].smooth == 127); + region.parseOpcode({ "amplitude_smoothcc14", "-2" }); + REQUIRE(region.amplitudeCC[14].smooth == 0); + region.parseOpcode({ "amplitude_stepcc120", "24" }); + REQUIRE(region.amplitudeCC[120].step == 0.24_a); + region.parseOpcode({ "amplitude_stepcc120", "15482" }); + REQUIRE(region.amplitudeCC[120].step == 1.0_a); + region.parseOpcode({ "amplitude_stepcc120", "-2" }); + REQUIRE(region.amplitudeCC[120].step == 0.0f); } SECTION("volume_oncc/gain_cc") @@ -1459,13 +1531,31 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.volumeCC.empty()); region.parseOpcode({ "gain_cc1", "40" }); REQUIRE(region.volumeCC.contains(1)); - REQUIRE(region.volumeCC[1] == 40_a); + REQUIRE(region.volumeCC[1].value == 40_a); region.parseOpcode({ "volume_oncc2", "-76" }); REQUIRE(region.volumeCC.contains(2)); - REQUIRE(region.volumeCC[2] == -76.0_a); + REQUIRE(region.volumeCC[2].value == -76.0_a); region.parseOpcode({ "gain_oncc4", "-1" }); REQUIRE(region.volumeCC.contains(4)); - REQUIRE(region.volumeCC[4] == -1.0_a); + REQUIRE(region.volumeCC[4].value == -1.0_a); + region.parseOpcode({ "volume_curvecc17", "18" }); + REQUIRE(region.volumeCC[17].curve == 18); + region.parseOpcode({ "volume_curvecc17", "15482" }); + REQUIRE(region.volumeCC[17].curve == 255); + region.parseOpcode({ "volume_curvecc17", "-2" }); + REQUIRE(region.volumeCC[17].curve == 0); + region.parseOpcode({ "volume_smoothcc14", "85" }); + REQUIRE(region.volumeCC[14].smooth == 85); + region.parseOpcode({ "volume_smoothcc14", "15482" }); + REQUIRE(region.volumeCC[14].smooth == 127); + region.parseOpcode({ "volume_smoothcc14", "-2" }); + REQUIRE(region.volumeCC[14].smooth == 0); + region.parseOpcode({ "volume_stepcc120", "24" }); + REQUIRE(region.volumeCC[120].step == 24.0f); + region.parseOpcode({ "volume_stepcc120", "15482" }); + REQUIRE(region.volumeCC[120].step == 48.0f); + region.parseOpcode({ "volume_stepcc120", "-2" }); + REQUIRE(region.volumeCC[120].step == 0.0f); } SECTION("tune_cc/pitch_cc") @@ -1473,13 +1563,31 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.tuneCC.empty()); region.parseOpcode({ "pitch_cc1", "40" }); REQUIRE(region.tuneCC.contains(1)); - REQUIRE(region.tuneCC[1] == 40); + REQUIRE(region.tuneCC[1].value == 40.0); region.parseOpcode({ "tune_oncc2", "-76" }); REQUIRE(region.tuneCC.contains(2)); - REQUIRE(region.tuneCC[2] == -76.0); + REQUIRE(region.tuneCC[2].value == -76.0); region.parseOpcode({ "pitch_oncc4", "-1" }); REQUIRE(region.tuneCC.contains(4)); - REQUIRE(region.tuneCC[4] == -1.0); + REQUIRE(region.tuneCC[4].value == -1.0); + region.parseOpcode({ "tune_curvecc17", "18" }); + REQUIRE(region.tuneCC[17].curve == 18); + region.parseOpcode({ "pitch_curvecc17", "15482" }); + REQUIRE(region.tuneCC[17].curve == 255); + region.parseOpcode({ "tune_curvecc17", "-2" }); + REQUIRE(region.tuneCC[17].curve == 0); + region.parseOpcode({ "pitch_smoothcc14", "85" }); + REQUIRE(region.tuneCC[14].smooth == 85); + region.parseOpcode({ "tune_smoothcc14", "15482" }); + REQUIRE(region.tuneCC[14].smooth == 127); + region.parseOpcode({ "pitch_smoothcc14", "-2" }); + REQUIRE(region.tuneCC[14].smooth == 0); + region.parseOpcode({ "tune_stepcc120", "24" }); + REQUIRE(region.tuneCC[120].step == 24.0f); + region.parseOpcode({ "pitch_stepcc120", "15482" }); + REQUIRE(region.tuneCC[120].step == 9600.0f); + region.parseOpcode({ "tune_stepcc120", "-2" }); + REQUIRE(region.tuneCC[120].step == 0.0f); } } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index bd0d28cc..6d7022ba 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -140,10 +140,11 @@ TEST_CASE("[Synth] All notes offs/all sounds off") TEST_CASE("[Synth] Reset all controllers") { sfz::Synth synth; + const auto& midiState = synth.getResources().midiState; synth.cc(0, 12, 64); - REQUIRE(synth.getMidiState().getCCValue(12) == 64_norm); + REQUIRE(midiState.getCCValue(12) == 64_norm); synth.cc(0, 121, 64); - REQUIRE(synth.getMidiState().getCCValue(12) == 0_norm); + REQUIRE(midiState.getCCValue(12) == 0_norm); } TEST_CASE("[Synth] Releasing before the EG started smoothing (initial delay) kills the voice") @@ -419,3 +420,17 @@ TEST_CASE("[Synth] Polyphony in master") synth.noteOn(0, 61, 64); REQUIRE(synth.getNumActiveVoices() == 3); } + +TEST_CASE("[Synth] Basic curves") +{ + sfz::Synth synth; + const auto& curves = synth.getResources().curves; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/curves.sfz"); + REQUIRE( synth.getNumCurves() == 19 ); + REQUIRE( curves.getCurve(18).evalCC7(127) == 1.0f ); + REQUIRE( curves.getCurve(18).evalCC7(95) == 0.5f ); + REQUIRE( curves.getCurve(17).evalCC7(100) == 1.0f ); + REQUIRE( curves.getCurve(17).evalCC7(95) == 0.5f ); + // Default linear + REQUIRE( curves.getCurve(16).evalCC7(63) == Approx(63_norm) ); +} diff --git a/tests/TestFiles/curves.sfz b/tests/TestFiles/curves.sfz new file mode 100644 index 00000000..8c24cca4 --- /dev/null +++ b/tests/TestFiles/curves.sfz @@ -0,0 +1,10 @@ + sample=*sine +curve_index=18 +v000=0 +v095=0.5 +v127=1 + +curve_index=17 +v000=0 +v095=0.5 +v100=1