Add support for curvecc opcodes on v1 EGs

This commit is contained in:
Python Blue 2023-06-08 22:19:09 -04:00 committed by Paul Ferrand
parent 000b263344
commit adefb7d60e
8 changed files with 442 additions and 144 deletions

@ -1 +1 @@
Subproject commit 78e75a8527fffe2f87cdd56d5989ff01c0723ef7 Subproject commit 36b08dbcb1de136e9ae477e9f1e2b57c958fff18

View file

@ -58,13 +58,13 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, int de
void ADSREnvelope::updateValues(int delay) noexcept void ADSREnvelope::updateValues(int delay) noexcept
{ {
if (currentState == State::Delay) if (currentState == State::Delay)
this->delay = delay + secondsToSamples(desc_->getDelay(midiState_, triggerVelocity_, delay)); this->delay = delay + secondsToSamples(desc_->getDelay(midiState_, curveSet_, triggerVelocity_, delay));
this->attackStep = secondsToLinRate(desc_->getAttack(midiState_, triggerVelocity_, delay)); this->attackStep = secondsToLinRate(desc_->getAttack(midiState_, curveSet_, triggerVelocity_, delay));
this->decayRate = secondsToExpRate(desc_->getDecay(midiState_, triggerVelocity_, delay)); this->decayRate = secondsToExpRate(desc_->getDecay(midiState_, curveSet_, triggerVelocity_, delay));
this->releaseRate = secondsToExpRate(desc_->getRelease(midiState_, triggerVelocity_, delay)); this->releaseRate = secondsToExpRate(desc_->getRelease(midiState_, curveSet_, triggerVelocity_, delay));
this->hold = secondsToSamples(desc_->getHold(midiState_, triggerVelocity_, delay)); this->hold = secondsToSamples(desc_->getHold(midiState_, curveSet_, triggerVelocity_, delay));
this->sustain = clamp(desc_->getSustain(midiState_, triggerVelocity_, delay), 0.0f, 1.0f); this->sustain = clamp(desc_->getSustain(midiState_, curveSet_, triggerVelocity_, delay), 0.0f, 1.0f);
this->start = clamp(desc_->getStart(midiState_, triggerVelocity_, delay), 0.0f, 1.0f); this->start = clamp(desc_->getStart(midiState_, curveSet_, triggerVelocity_, delay), 0.0f, 1.0f);
sustainThreshold = this->sustain + config::virtuallyZero; sustainThreshold = this->sustain + config::virtuallyZero;
} }

View file

@ -14,12 +14,13 @@ namespace sfz {
* @brief Describe an attack/delay/sustain/release envelope that can * @brief Describe an attack/delay/sustain/release envelope that can
* produce its coefficient in a blockwise manner for SIMD-type operations. * produce its coefficient in a blockwise manner for SIMD-type operations.
*/ */
class ADSREnvelope { class ADSREnvelope {
public: public:
using Float = float; using Float = float;
ADSREnvelope(const MidiState& state) ADSREnvelope(const MidiState& state, CurveSet& curveSet)
: midiState_(state) {} : midiState_(state), curveSet_(curveSet) {}
/** /**
* @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and * @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and
* trigger velocity * trigger velocity
@ -104,6 +105,7 @@ private:
Float currentValue { 0.0 }; Float currentValue { 0.0 };
const EGDescription* desc_ { nullptr }; const EGDescription* desc_ { nullptr };
const MidiState& midiState_; const MidiState& midiState_;
CurveSet& curveSet_;
float triggerVelocity_ { 0.0f }; float triggerVelocity_ { 0.0f };
bool dynamic_ { false }; bool dynamic_ { false };
int delay { 0 }; int delay { 0 };

View file

@ -32,6 +32,8 @@
#include "utility/LeakDetector.h" #include "utility/LeakDetector.h"
#include <absl/types/optional.h> #include <absl/types/optional.h>
#include "Curve.h"
namespace sfz { namespace sfz {
/** /**
* @brief A description for an SFZ envelope generator, with its envelope parameters * @brief A description for an SFZ envelope generator, with its envelope parameters
@ -66,13 +68,20 @@ struct EGDescription {
float vel2sustain { Default::egPercentMod }; float vel2sustain { Default::egPercentMod };
float vel2depth { Default::egVel2Depth }; float vel2depth { Default::egVel2Depth };
CCMap<float> ccAttack; CCMap<ModifierCurvePair<float>> ccAttack { ModifierCurvePair<float>{ Default::egTime, Default::curveCC } };
CCMap<float> ccDecay; CCMap<ModifierCurvePair<float>> ccDecay { ModifierCurvePair<float>{ Default::egTime, Default::curveCC } };
CCMap<float> ccDelay; CCMap<ModifierCurvePair<float>> ccDelay { ModifierCurvePair<float>{ Default::egTime, Default::curveCC } };
CCMap<float> ccHold; CCMap<ModifierCurvePair<float>> ccHold { ModifierCurvePair<float>{ Default::egTime, Default::curveCC } };
CCMap<float> ccRelease; CCMap<ModifierCurvePair<float>> ccRelease { ModifierCurvePair<float>{ Default::egTime, Default::curveCC } };
CCMap<float> ccStart; CCMap<ModifierCurvePair<float>> ccStart { ModifierCurvePair<float>{ Default::egPercentMod, Default::curveCC } };
CCMap<float> ccSustain; CCMap<ModifierCurvePair<float>> ccSustain { ModifierCurvePair<float>{ Default:: egPercentMod, Default::curveCC } };
//CCMap<float> ccAttack;
//CCMap<float> ccDecay;
//CCMap<float> ccDelay;
//CCMap<float> ccHold;
//CCMap<float> ccRelease;
//CCMap<float> ccStart;
//CCMap<float> ccSustain;
bool dynamic { false }; bool dynamic { false };
/** /**
@ -82,12 +91,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getAttack(const MidiState& state, float velocity, int delay = 0) const noexcept float getAttack(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { attack + velocity * vel2attack }; float returnedValue { attack + velocity * vel2attack };
for (auto& mod: ccAttack) { for (auto& mod: ccAttack) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -98,12 +108,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getDecay(const MidiState& state, float velocity, int delay = 0) const noexcept float getDecay(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { decay + velocity * vel2decay }; float returnedValue { decay + velocity * vel2decay };
for (auto& mod: ccDecay) { for (auto& mod: ccDecay) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -114,12 +125,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getDelay(const MidiState& state, float velocity, int delay = 0) const noexcept float getDelay(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { this->delay + velocity * vel2delay }; float returnedValue { this->delay + velocity * vel2delay };
for (auto& mod: ccDelay) { for (auto& mod: ccDelay) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -130,12 +142,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getHold(const MidiState& state, float velocity, int delay = 0) const noexcept float getHold(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { hold + velocity * vel2hold }; float returnedValue { hold + velocity * vel2hold };
for (auto& mod: ccHold) { for (auto& mod: ccHold) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -146,12 +159,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getRelease(const MidiState& state, float velocity, int delay = 0) const noexcept float getRelease(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { release + velocity * vel2release }; float returnedValue { release + velocity * vel2release };
for (auto& mod: ccRelease) { for (auto& mod: ccRelease) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -162,12 +176,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getStart(const MidiState& state, float velocity, int delay = 0) const noexcept float getStart(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
UNUSED(velocity); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { start }; float returnedValue { start };
for (auto& mod: ccStart) { for (auto& mod: ccStart) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }
@ -178,12 +193,13 @@ struct EGDescription {
* @param velocity * @param velocity
* @return float * @return float
*/ */
float getSustain(const MidiState& state, float velocity, int delay = 0) const noexcept float getSustain(const MidiState& state, CurveSet& curveSet, float velocity, int delay = 0) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float returnedValue { sustain + velocity * vel2sustain }; float returnedValue { sustain + velocity * vel2sustain };
for (auto& mod: ccSustain) { for (auto& mod: ccSustain) {
returnedValue += state.getCCValueAt(mod.cc, delay) * mod.data; const auto& curve = curveSet.getCurve(mod.data.curve);
returnedValue += curve.evalNormalized(state.getCCValueAt(mod.cc, delay)) * mod.data.modifier;
} }
return returnedValue; return returnedValue;
} }

View file

@ -1110,49 +1110,98 @@ bool sfz::Region::parseEGOpcode(const Opcode& opcode, EGDescription& eg)
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccAttack[opcode.parameters.back()] = opcode.read(Default::egTimeMod); eg.ccAttack[opcode.parameters.back()].modifier = opcode.read(Default::egTimeMod);
break;
case_any_eg("attack_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccAttack[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("decay_oncc&"): // also decaycc& case_any_eg("decay_oncc&"): // also decaycc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccDecay[opcode.parameters.back()] = opcode.read(Default::egTimeMod); eg.ccDecay[opcode.parameters.back()].modifier = opcode.read(Default::egTimeMod);
break;
case_any_eg("decay_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccDecay[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("delay_oncc&"): // also delaycc& case_any_eg("delay_oncc&"): // also delaycc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccDelay[opcode.parameters.back()] = opcode.read(Default::egTimeMod); eg.ccDelay[opcode.parameters.back()].modifier = opcode.read(Default::egTimeMod);
break;
case_any_eg("delay_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccDelay[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("hold_oncc&"): // also holdcc& case_any_eg("hold_oncc&"): // also holdcc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccHold[opcode.parameters.back()] = opcode.read(Default::egTimeMod); eg.ccHold[opcode.parameters.back()].modifier = opcode.read(Default::egTimeMod);
break;
case_any_eg("hold_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccHold[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("release_oncc&"): // also releasecc& case_any_eg("release_oncc&"): // also releasecc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccRelease[opcode.parameters.back()] = opcode.read(Default::egTimeMod); eg.ccRelease[opcode.parameters.back()].modifier = opcode.read(Default::egTimeMod);
break;
case_any_eg("release_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccRelease[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("start_oncc&"): // also startcc& case_any_eg("start_oncc&"): // also startcc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccStart[opcode.parameters.back()] = opcode.read(Default::egPercentMod); eg.ccStart[opcode.parameters.back()].modifier = opcode.read(Default::egPercentMod);
break;
case_any_eg("start_curvecc&"): // also startcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccStart[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;
case_any_eg("sustain_oncc&"): // also sustaincc& case_any_eg("sustain_oncc&"): // also sustaincc&
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;
eg.ccSustain[opcode.parameters.back()] = opcode.read(Default::egPercentMod); eg.ccSustain[opcode.parameters.back()].modifier = opcode.read(Default::egPercentMod);
break;
case_any_eg("sustain_curvecc&"): // also attackcc&
if (opcode.parameters.back() >= config::numCCs)
return false;
eg.ccSustain[opcode.parameters.back()].curve = opcode.read(Default::curveCC);
break; break;

View file

@ -1222,6 +1222,90 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
} }
} break; } break;
MATCH("/region&/pitcheg_attack_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccAttack.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/pitcheg_decay_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccDecay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/pitcheg_delay_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccDelay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/pitcheg_hold_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccHold.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/pitcheg_release_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccRelease.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/pitcheg_start_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccStart.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break;
MATCH("/region&/pitcheg_sustain_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccSustain.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break;
MATCH("/region&/pitcheg_attack_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccAttack.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_decay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccDecay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_delay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccDelay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_hold_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccHold.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_release_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccRelease.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_start_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccStart.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/pitcheg_sustain_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.pitchEG->ccSustain.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/note_polyphony", "") { MATCH("/region&/note_polyphony", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
if (region.notePolyphony) { if (region.notePolyphony) {
@ -1338,44 +1422,86 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
MATCH("/region&/ampeg_attack_cc&", "") { MATCH("/region&/ampeg_attack_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccAttack.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccAttack.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value); client.receive<'f'>(delay, path, cc.modifier);
} break; } break;
MATCH("/region&/ampeg_decay_cc&", "") { MATCH("/region&/ampeg_decay_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccDecay.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccDecay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value); client.receive<'f'>(delay, path, cc.modifier);
} break; } break;
MATCH("/region&/ampeg_delay_cc&", "") { MATCH("/region&/ampeg_delay_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccDelay.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccDelay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value); client.receive<'f'>(delay, path, cc.modifier);
} break; } break;
MATCH("/region&/ampeg_hold_cc&", "") { MATCH("/region&/ampeg_hold_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccHold.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccHold.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value); client.receive<'f'>(delay, path, cc.modifier);
} break; } break;
MATCH("/region&/ampeg_release_cc&", "") { MATCH("/region&/ampeg_release_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccRelease.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccRelease.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value); client.receive<'f'>(delay, path, cc.modifier);
} break; } break;
MATCH("/region&/ampeg_start_cc&", "") { MATCH("/region&/ampeg_start_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccStart.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccStart.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value * 100.0f); client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break; } break;
MATCH("/region&/ampeg_sustain_cc&", "") { MATCH("/region&/ampeg_sustain_cc&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
float value = region.amplitudeEG.ccSustain.getWithDefault(indices[1]); const auto& cc = region.amplitudeEG.ccSustain.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, value * 100.0f); client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break;
MATCH("/region&/ampeg_attack_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccAttack.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_decay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccDecay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_delay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccDelay.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_hold_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccHold.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_release_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccRelease.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_start_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccStart.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/ampeg_sustain_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
const auto& cc = region.amplitudeEG.ccSustain.getWithDefault(indices[1]);
client.receive<'f'>(delay, path, cc.curve);
} break; } break;
MATCH("/region&/filter&/cutoff", "") { MATCH("/region&/filter&/cutoff", "") {
@ -1495,6 +1621,104 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
} }
} break; } break;
MATCH("/region&/filter&/fileg_attack_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccAttack.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/filter&/fileg_decay_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccDecay.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/filter&/fileg_delay_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccDelay.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/filter&/fileg_hold_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccHold.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/filter&/fileg_release_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccRelease.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier);
} break;
MATCH("/region&/filter&/fileg_start_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccStart.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break;
MATCH("/region&/filter&/fileg_sustain_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccSustain.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.modifier * 100.0f);
} break;
MATCH("/region&/filter&/fileg_attack_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccAttack.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_decay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccDecay.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_delay_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccDelay.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_hold_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccHold.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_release_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccRelease.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_start_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccStart.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/filter&/fileg_sustain_curvecc&", "") {
GET_REGION_OR_BREAK(indices[0])
GET_FILTER_OR_BREAK(indices[1])
const auto& cc = region.filterEG->ccSustain.getWithDefault(indices[2]);
client.receive<'f'>(delay, path, cc.curve);
} break;
MATCH("/region&/eq&/gain", "") { MATCH("/region&/eq&/gain", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
GET_EQ_OR_BREAK(indices[1]) GET_EQ_OR_BREAK(indices[1])

View file

@ -275,7 +275,7 @@ struct Voice::Impl
std::unique_ptr<LFO> lfoPitch_; std::unique_ptr<LFO> lfoPitch_;
std::unique_ptr<LFO> lfoFilter_; std::unique_ptr<LFO> lfoFilter_;
ADSREnvelope egAmplitude_ { resources_.getMidiState() }; ADSREnvelope egAmplitude_ { resources_.getMidiState(), resources_.getCurves() };
std::unique_ptr<ADSREnvelope> egPitch_; std::unique_ptr<ADSREnvelope> egPitch_;
std::unique_ptr<ADSREnvelope> egFilter_; std::unique_ptr<ADSREnvelope> egFilter_;
@ -1853,7 +1853,7 @@ void Voice::setPitchEGEnabledPerVoice(bool havePitchEG)
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
if (havePitchEG) if (havePitchEG)
impl.egPitch_.reset(new ADSREnvelope(impl.resources_.getMidiState())); impl.egPitch_.reset(new ADSREnvelope(impl.resources_.getMidiState(), impl.resources_.getCurves()));
else else
impl.egPitch_.reset(); impl.egPitch_.reset();
} }
@ -1862,7 +1862,7 @@ void Voice::setFilterEGEnabledPerVoice(bool haveFilterEG)
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
if (haveFilterEG) if (haveFilterEG)
impl.egFilter_.reset(new ADSREnvelope(impl.resources_.getMidiState())); impl.egFilter_.reset(new ADSREnvelope(impl.resources_.getMidiState(), impl.resources_.getCurves()));
else else
impl.egFilter_.reset(); impl.egFilter_.reset();
} }

View file

@ -15,158 +15,165 @@ TEST_CASE("[EGDescription] Attack range")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.attack = 1; eg.attack = 1;
eg.vel2attack = -1.27f; eg.vel2attack = -1.27f;
eg.ccAttack[63] = 1.27f; eg.ccAttack[63].modifier = 1.27f;
REQUIRE(eg.getAttack(state, 0_norm) == 1.0f); REQUIRE(eg.getAttack(state, curveSet, 0_norm) == 1.0f);
//REQUIRE(eg.getAttack(state, 127_norm) == 0.0f); //REQUIRE(eg.getAttack(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f); REQUIRE(eg.getAttack(state, curveSet, 127_norm) == 1.0f);
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f); REQUIRE(eg.getAttack(state, curveSet, 0_norm) == 2.27f);
//eg.ccAttack[63] = 127.0f; //eg.ccAttack[63].modifier = 127.0f;
//REQUIRE(eg.getAttack(state, 0_norm) == 100.0f); //REQUIRE(eg.getAttack(state, curveSet, 0_norm) == 100.0f);
eg.ccAttack[63] = 1.27f; eg.ccAttack[63].modifier = 1.27f;
eg.ccAttack[65] = 1.0f; eg.ccAttack[65].modifier = 1.0f;
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f); REQUIRE(eg.getAttack(state, curveSet, 0_norm) == 2.27f);
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f); REQUIRE(eg.getAttack(state, curveSet, 127_norm) == 1.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getAttack(state, 0_norm) == 3.27f); REQUIRE(eg.getAttack(state, curveSet, 0_norm) == 3.27f);
REQUIRE(eg.getAttack(state, 127_norm) == 2.0f); REQUIRE(eg.getAttack(state, curveSet, 127_norm) == 2.0f);
} }
TEST_CASE("[EGDescription] Delay range") TEST_CASE("[EGDescription] Delay range")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.delay = 1; eg.delay = 1;
eg.vel2delay = -1.27f; eg.vel2delay = -1.27f;
eg.ccDelay[63] = 1.27f; eg.ccDelay[63].modifier = 1.27f;
REQUIRE(eg.getDelay(state, 0_norm) == 1.0f); REQUIRE(eg.getDelay(state, curveSet, 0_norm) == 1.0f);
//REQUIRE(eg.getDelay(state, 127_norm) == 0.0f); //REQUIRE(eg.getDelay(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f); REQUIRE(eg.getDelay(state, curveSet, 127_norm) == 1.0f);
REQUIRE(eg.getDelay(state, 0_norm, 1) == 2.27f); REQUIRE(eg.getDelay(state, curveSet, 0_norm, 1) == 2.27f);
//eg.ccDelay[63] = 127.0f; //eg.ccDelay[63].modifier = 127.0f;
//REQUIRE(eg.getDelay(state, 0_norm) == 100.0f); //REQUIRE(eg.getDelay(state, curveSet, 0_norm) == 100.0f);
eg.ccDelay[63] = 1.27f; eg.ccDelay[63].modifier = 1.27f;
eg.ccDelay[65] = 1.0f; eg.ccDelay[65].modifier = 1.0f;
REQUIRE(eg.getDelay(state, 0_norm) == 2.27f); REQUIRE(eg.getDelay(state, curveSet, 0_norm) == 2.27f);
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f); REQUIRE(eg.getDelay(state, curveSet, 127_norm) == 1.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getDelay(state, 0_norm) == 3.27f); REQUIRE(eg.getDelay(state, curveSet, 0_norm) == 3.27f);
REQUIRE(eg.getDelay(state, 127_norm) == 2.0f); REQUIRE(eg.getDelay(state, curveSet, 127_norm) == 2.0f);
} }
TEST_CASE("[EGDescription] Decay range") TEST_CASE("[EGDescription] Decay range")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.decay = 1.0f; eg.decay = 1.0f;
eg.vel2decay = -1.27f; eg.vel2decay = -1.27f;
eg.ccDecay[63] = 1.27f; eg.ccDecay[63].modifier = 1.27f;
REQUIRE(eg.getDecay(state, 0_norm) == 1.0f); REQUIRE(eg.getDecay(state, curveSet, 0_norm) == 1.0f);
//REQUIRE(eg.getDecay(state, 127_norm) == 0.0f); //REQUIRE(eg.getDecay(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f); REQUIRE(eg.getDecay(state, curveSet, 127_norm) == 1.0f);
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f); REQUIRE(eg.getDecay(state, curveSet, 0_norm) == 2.27f);
//eg.ccDecay[63] = 127.0f; //eg.ccDecay[63].modifier = 127.0f;
//REQUIRE(eg.getDecay(state, 0_norm) == 100.0f); //REQUIRE(eg.getDecay(state, curveSet, 0_norm) == 100.0f);
eg.ccDecay[63] = 1.27f; eg.ccDecay[63].modifier = 1.27f;
eg.ccDecay[65] = 1.0f; eg.ccDecay[65].modifier = 1.0f;
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f); REQUIRE(eg.getDecay(state, curveSet, 0_norm) == 2.27f);
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f); REQUIRE(eg.getDecay(state, curveSet, 127_norm) == 1.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getDecay(state, 0_norm) == 3.27f); REQUIRE(eg.getDecay(state, curveSet, 0_norm) == 3.27f);
REQUIRE(eg.getDecay(state, 127_norm) == 2.0f); REQUIRE(eg.getDecay(state, curveSet, 127_norm) == 2.0f);
} }
TEST_CASE("[EGDescription] Release range") TEST_CASE("[EGDescription] Release range")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.release = 1; eg.release = 1;
eg.vel2release = -1.27f; eg.vel2release = -1.27f;
eg.ccRelease[63] = 1.27f; eg.ccRelease[63].modifier = 1.27f;
REQUIRE(eg.getRelease(state, 0_norm) == 1.0f); REQUIRE(eg.getRelease(state, curveSet, 0_norm) == 1.0f);
//REQUIRE(eg.getRelease(state, 127_norm) == 0.0f); //REQUIRE(eg.getRelease(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f); REQUIRE(eg.getRelease(state, curveSet, 127_norm) == 1.0f);
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f); REQUIRE(eg.getRelease(state, curveSet, 0_norm) == 2.27f);
//eg.ccRelease[63] = 127.0f; //eg.ccRelease[63].modifier = 127.0f;
//REQUIRE(eg.getRelease(state, 0_norm) == 100.0f); //REQUIRE(eg.getRelease(state, curveSet, 0_norm) == 100.0f);
eg.ccRelease[63] = 1.27f; eg.ccRelease[63].modifier = 1.27f;
eg.ccRelease[65] = 1.0f; eg.ccRelease[65].modifier = 1.0f;
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f); REQUIRE(eg.getRelease(state, curveSet, 0_norm) == 2.27f);
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f); REQUIRE(eg.getRelease(state, curveSet, 127_norm) == 1.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getRelease(state, 0_norm) == 3.27f); REQUIRE(eg.getRelease(state, curveSet, 0_norm) == 3.27f);
REQUIRE(eg.getRelease(state, 127_norm) == 2.0f); REQUIRE(eg.getRelease(state, curveSet, 127_norm) == 2.0f);
} }
TEST_CASE("[EGDescription] Hold range") TEST_CASE("[EGDescription] Hold range")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.hold = 1; eg.hold = 1;
eg.vel2hold = -1.27f; eg.vel2hold = -1.27f;
eg.ccHold[63] = 1.27f; eg.ccHold[63].modifier = 1.27f;
REQUIRE(eg.getHold(state, 0_norm) == 1.0f); REQUIRE(eg.getHold(state, curveSet, 0_norm) == 1.0f);
//REQUIRE(eg.getHold(state, 127_norm) == 0.0f); //REQUIRE(eg.getHold(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getHold(state, 127_norm) == 1.0f); REQUIRE(eg.getHold(state, curveSet, 127_norm) == 1.0f);
REQUIRE(eg.getHold(state, 0_norm) == 2.27f); REQUIRE(eg.getHold(state, curveSet, 0_norm) == 2.27f);
//eg.ccHold[63] = 127.0f; //eg.ccHold[63].modifier = 127.0f;
//REQUIRE(eg.getHold(state, 0_norm) == 100.0f); //REQUIRE(eg.getHold(state, curveSet, 0_norm) == 100.0f);
eg.ccHold[63] = 1.27f; eg.ccHold[63].modifier = 1.27f;
eg.ccHold[65] = 1.0f; eg.ccHold[65].modifier = 1.0f;
REQUIRE(eg.getHold(state, 0_norm) == 2.27f); REQUIRE(eg.getHold(state, curveSet, 0_norm) == 2.27f);
REQUIRE(eg.getHold(state, 127_norm) == 1.0f); REQUIRE(eg.getHold(state, curveSet, 127_norm) == 1.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getHold(state, 0_norm) == 3.27f); REQUIRE(eg.getHold(state, curveSet, 0_norm) == 3.27f);
REQUIRE(eg.getHold(state, 127_norm) == 2.0f); REQUIRE(eg.getHold(state, curveSet, 127_norm) == 2.0f);
} }
TEST_CASE("[EGDescription] Sustain level") TEST_CASE("[EGDescription] Sustain level")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.sustain = 50; eg.sustain = 50;
eg.vel2sustain = -100; eg.vel2sustain = -100;
eg.ccSustain[63] = 100.0f; eg.ccSustain[63].modifier = 100.0f;
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f); REQUIRE(eg.getSustain(state, curveSet, 0_norm) == 50.0f);
//REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); //REQUIRE(eg.getSustain(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE(eg.getSustain(state, 127_norm) == 50.0f); REQUIRE(eg.getSustain(state, curveSet, 127_norm) == 50.0f);
//eg.ccSustain[63] = 200.0f; //eg.ccSustain[63].modifier = 200.0f;
//REQUIRE(eg.getSustain(state, 0_norm) == 100.0f); //REQUIRE(eg.getSustain(state, curveSet, 0_norm) == 100.0f);
eg.sustain = 0; eg.sustain = 0;
eg.ccSustain[63] = 50.0f; eg.ccSustain[63].modifier = 50.0f;
eg.ccSustain[65] = 50.0f; eg.ccSustain[65].modifier = 50.0f;
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f); REQUIRE(eg.getSustain(state, curveSet, 0_norm) == 50.0f);
//REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); //REQUIRE(eg.getSustain(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getSustain(state, 0_norm) == 100.0f); REQUIRE(eg.getSustain(state, curveSet, 0_norm) == 100.0f);
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); REQUIRE(eg.getSustain(state, curveSet, 127_norm) == 0.0f);
} }
TEST_CASE("[EGDescription] Start level") TEST_CASE("[EGDescription] Start level")
{ {
sfz::EGDescription eg; sfz::EGDescription eg;
sfz::MidiState state; sfz::MidiState state;
sfz::CurveSet curveSet;
eg.start = 0; eg.start = 0;
eg.ccStart[63] = 127.0f; eg.ccStart[63].modifier = 127.0f;
REQUIRE(eg.getStart(state, 0_norm) == 0.0f); REQUIRE(eg.getStart(state, curveSet, 0_norm) == 0.0f);
REQUIRE(eg.getStart(state, 127_norm) == 0.0f); REQUIRE(eg.getStart(state, curveSet, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
//REQUIRE(eg.getStart(state, 0_norm) == 100.0f); //REQUIRE(eg.getStart(state, curveSet, 0_norm) == 100.0f);
//eg.ccStart[63] = -127.0f; //eg.ccStart[63] = -127.0f;
//REQUIRE(eg.getStart(state, 0_norm) == 0.0f); //REQUIRE(eg.getStart(state, curveSet, 0_norm) == 0.0f);
eg.start = 0; eg.start = 0;
eg.ccStart[63] = 50.0f; eg.ccStart[63].modifier = 50.0f;
eg.ccStart[65] = 50.0f; eg.ccStart[65].modifier = 50.0f;
REQUIRE(eg.getStart(state, 0_norm) == 50.0f); REQUIRE(eg.getStart(state, curveSet, 0_norm) == 50.0f);
REQUIRE(eg.getStart(state, 127_norm) == 50.0f); REQUIRE(eg.getStart(state, curveSet, 127_norm) == 50.0f);
state.ccEvent(0, 65, 127_norm); state.ccEvent(0, 65, 127_norm);
REQUIRE(eg.getStart(state, 0_norm) == 100.0f); REQUIRE(eg.getStart(state, curveSet, 0_norm) == 100.0f);
REQUIRE(eg.getStart(state, 127_norm) == 100.0f); REQUIRE(eg.getStart(state, curveSet, 127_norm) == 100.0f);
} }