diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index 5019f1fe..40ce37c7 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -152,7 +152,9 @@ BoolSpec flexEGAmpeg { false, {0, 1}, kEnforceBounds }; BoolSpec flexEGDynamic { 0, {0, 1}, kEnforceBounds }; Int32Spec flexEGSustain { 0, {0, 100}, kEnforceLowerBound|kPermissiveUpperBound }; FloatSpec flexEGPointTime { 0.0f, {0.0f, 100.0f}, kPermissiveBounds }; +FloatSpec flexEGPointTimeMod { 0.0f, {-100.0f, 100.0f}, kPermissiveBounds }; FloatSpec flexEGPointLevel { 0.0f, {-1.0f, 1.0f}, kPermissiveBounds }; +FloatSpec flexEGPointLevelMod { 0.0f, {-1.0f, 1.0f}, kPermissiveBounds }; FloatSpec flexEGPointShape { 0.0f, {-100.0f, 100.0f}, kPermissiveBounds }; Int32Spec sampleQuality { 2, {0, 10}, 0 }; Int32Spec octaveOffset { 0, {-10, 10}, kPermissiveBounds }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 41a72607..7d6c54fa 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -264,7 +264,9 @@ namespace Default extern const OpcodeSpec flexEGDynamic; extern const OpcodeSpec flexEGSustain; extern const OpcodeSpec flexEGPointTime; + extern const OpcodeSpec flexEGPointTimeMod; extern const OpcodeSpec flexEGPointLevel; + extern const OpcodeSpec flexEGPointLevelMod; extern const OpcodeSpec flexEGPointShape; extern const OpcodeSpec sampleQuality; extern const OpcodeSpec octaveOffset; diff --git a/src/sfizz/FlexEGDescription.cpp b/src/sfizz/FlexEGDescription.cpp index 18ea92e3..f92e20eb 100644 --- a/src/sfizz/FlexEGDescription.cpp +++ b/src/sfizz/FlexEGDescription.cpp @@ -6,6 +6,7 @@ #include "FlexEGDescription.h" #include "Curve.h" +#include "MidiState.h" #include #include @@ -84,4 +85,21 @@ void FlexEGs::clearUnusedCurves() } } +/// +float FlexEGPoint::getTime(const MidiState& state) const noexcept +{ + float returnedValue { time }; + for (const CCData& mod : ccTime) + returnedValue += state.getCCValue(mod.cc) * mod.data; + return returnedValue; +} + +float FlexEGPoint::getLevel(const MidiState& state) const noexcept +{ + float returnedValue { level }; + for (const CCData& mod : ccLevel) + returnedValue += state.getCCValue(mod.cc) * mod.data; + return returnedValue; +} + } // namespace sfz diff --git a/src/sfizz/FlexEGDescription.h b/src/sfizz/FlexEGDescription.h index a4cc75ef..4c816bef 100644 --- a/src/sfizz/FlexEGDescription.h +++ b/src/sfizz/FlexEGDescription.h @@ -6,11 +6,13 @@ #pragma once #include "Defaults.h" +#include "CCMap.h" #include #include namespace sfz { class Curve; +class MidiState; namespace FlexEGs { std::shared_ptr getShapeCurve(float shape); @@ -21,6 +23,12 @@ struct FlexEGPoint { float time { Default::flexEGPointTime }; // duration until next step (s) float level { Default::flexEGPointLevel }; // normalized amplitude + CCMap ccTime; + CCMap ccLevel; + + float getTime(const MidiState& state) const noexcept; + float getLevel(const MidiState& state) const noexcept; + void setShape(float shape); float shape() const noexcept { return shape_; } const Curve& curve() const; diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 98e09552..ae06c022 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -9,8 +9,11 @@ - [ ] egN_points (purpose unknown) - [x] egN_timeX +- [x] egN_timeX_onccY - [x] egN_levelX +- [x] egN_levelX_onccY - [x] egN_shapeX +- [ ] egN_shapeX_onccY - [x] egN_sustain - [ ] egN_dynamic - [ ] egN_loop @@ -21,6 +24,8 @@ #include "FlexEnvelope.h" #include "FlexEGDescription.h" #include "Curve.h" +#include "MidiState.h" +#include "Resources.h" #include "Config.h" #include "SIMDHelpers.h" #include @@ -28,6 +33,7 @@ namespace sfz { struct FlexEnvelope::Impl { + const Resources* resources_ { nullptr }; const FlexEGDescription* desc_ { nullptr }; float samplePeriod_ { 1.0 / config::defaultSampleRate }; size_t delayFramesLeft_ { 0 }; @@ -53,9 +59,11 @@ struct FlexEnvelope::Impl { bool advanceToNextStage(); }; -FlexEnvelope::FlexEnvelope() +FlexEnvelope::FlexEnvelope(Resources &resources) : impl_(new Impl) { + Impl& impl = *impl_; + impl.resources_ = &resources; } FlexEnvelope::~FlexEnvelope() @@ -249,6 +257,7 @@ void FlexEnvelope::Impl::process(absl::Span out) bool FlexEnvelope::Impl::advanceToStage(unsigned stageNumber) { const FlexEGDescription& desc = *desc_; + const MidiState& midiState = resources_->getMidiState(); currentStageNumber_ = stageNumber; @@ -257,8 +266,8 @@ bool FlexEnvelope::Impl::advanceToStage(unsigned stageNumber) const FlexEGPoint& point = desc.points[stageNumber]; stageSourceLevel_ = currentLevel_; - stageTargetLevel_ = point.level; - stageTime_ = point.time; + stageTargetLevel_ = point.getLevel(midiState); + stageTime_ = point.getTime(midiState); stageSustained_ = int(stageNumber) == desc.sustain; stageCurve_ = &point.curve(); diff --git a/src/sfizz/FlexEnvelope.h b/src/sfizz/FlexEnvelope.h index 6a737cf1..a72d280f 100644 --- a/src/sfizz/FlexEnvelope.h +++ b/src/sfizz/FlexEnvelope.h @@ -10,13 +10,14 @@ namespace sfz { struct FlexEGDescription; +class Resources; /** Flex envelope generator (according to ARIA) */ class FlexEnvelope { public: - FlexEnvelope(); + explicit FlexEnvelope(Resources &resources); ~FlexEnvelope(); /** diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index dc069413..344d7c6a 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1452,12 +1452,32 @@ bool sfz::Region::parseEGOpcodeV2(const Opcode& opcode) else return false; break; + case hash("eg&_time&_oncc&"): + if (FlexEGPoint* point = getOrCreateEGPoint()) { + auto ccNumber = opcode.parameters.back(); + if (ccNumber >= config::numCCs) + return false; + point->ccTime[ccNumber] = opcode.read(Default::flexEGPointTimeMod); + } + else + return false; + break; case hash("eg&_level&"): if (FlexEGPoint* point = getOrCreateEGPoint()) point->level = opcode.read(Default::flexEGPointLevel); else return false; break; + case hash("eg&_level&_oncc&"): + if (FlexEGPoint* point = getOrCreateEGPoint()) { + auto ccNumber = opcode.parameters.back(); + if (ccNumber >= config::numCCs) + return false; + point->ccLevel[ccNumber] = opcode.read(Default::flexEGPointLevelMod); + } + else + return false; + break; case hash("eg&_shape&"): if (FlexEGPoint* point = getOrCreateEGPoint()) point->setShape(opcode.read(Default::flexEGPointShape)); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f5d2a385..0e7f4dbe 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -2068,6 +2068,12 @@ void Synth::Impl::collectUsedCCsFromRegion(BitArray& usedCCs, co collectUsedCCsFromCCMap(usedCCs, lfo.delayCC); collectUsedCCsFromCCMap(usedCCs, lfo.fadeCC); } + for (const FlexEGDescription& flexEG : region.flexEGs) { + for (const FlexEGPoint& point : flexEG.points) { + collectUsedCCsFromCCMap(usedCCs, point.ccTime); + collectUsedCCsFromCCMap(usedCCs, point.ccLevel); + } + } collectUsedCCsFromCCMap(usedCCs, region.ccConditions); collectUsedCCsFromCCMap(usedCCs, region.ccTriggers); collectUsedCCsFromCCMap(usedCCs, region.crossfadeCCInRange); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 467c09b4..14f14881 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -1817,10 +1817,12 @@ void Voice::setMaxLFOsPerVoice(size_t numLFOs) void Voice::setMaxFlexEGsPerVoice(size_t numFlexEGs) { Impl& impl = *impl_; + Resources& resources = impl.resources_; + impl.flexEGs_.resize(numFlexEGs); for (size_t i = 0; i < numFlexEGs; ++i) { - auto eg = absl::make_unique(); + auto eg = absl::make_unique(resources); eg->setSampleRate(impl.sampleRate_); impl.flexEGs_[i] = std::move(eg); } diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index 76657eea..a35711e6 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -107,7 +107,7 @@ TEST_CASE("[FlexEG] Coarse numerical envelope test (No release)") eg1_time2=0.5 eg1_level2=1 eg1_sustain=2 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -132,7 +132,7 @@ TEST_CASE("[FlexEG] Detailed numerical envelope test") eg1_time2=0.5 eg1_level2=1 eg1_sustain=2 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -155,7 +155,7 @@ TEST_CASE("[FlexEG] Coarse numerical envelope test (with release)") eg1_time2=0.5 eg1_level2=1 eg1_sustain=2 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -184,7 +184,7 @@ TEST_CASE("[FlexEG] Detailed numerical envelope test (with release and release r eg1_time3=0.5 eg1_level3=0 eg1_sustain=2 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -216,7 +216,7 @@ TEST_CASE("[FlexEG] Coarse numerical envelope test (with shapes)") eg1_sustain=2 eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -244,7 +244,7 @@ TEST_CASE("[FlexEG] Detailed numerical envelope test (with shapes)") eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 eg1_sustain=2 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -276,7 +276,7 @@ TEST_CASE("[FlexEG] Zero delay transitions") eg1_time3=1 eg1_level3=.5 eg1_sustain=3 eg1_time4=1 eg1_level4=1 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->flexEGs.size() == 1); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -303,7 +303,7 @@ TEST_CASE("[FlexEG] Early release") eg1_time2=1.0 eg1_level2=1.0 eg1_sustain=2 eg1_time3=1.0 eg1_level3=0.0 )"); - sfz::FlexEnvelope envelope; + sfz::FlexEnvelope envelope(synth.getResources()); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->flexEGs.size() == 1); envelope.configure(&synth.getRegionView(0)->flexEGs[0]); @@ -417,3 +417,43 @@ TEST_CASE("[FlexEG] Free-running flex AmpEG (no sustain)") synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices() == 0 ); } + +TEST_CASE("[FlexEG] Modulation of time and level") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*noise + eg1_time1=0 eg1_level1=1 + eg1_time2=0.7 eg1_level2=0.5 + eg1_time2_oncc1=-0.7 eg1_level2_oncc2=0.5 + eg1_time3=0.3 eg1_level3=0.0 + )"); + + REQUIRE( synth.getNumRegions() == 1 ); + const sfz::Region* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 1 ); + const sfz::FlexEGDescription& desc = synth.getRegionView(0)->flexEGs[0]; + REQUIRE( desc.points.size() == 4 ); + + REQUIRE( desc.points[2].time == Approx(0.7f) ); + REQUIRE( desc.points[2].level == Approx(0.5f) ); + + sfz::MidiState state; + + REQUIRE( desc.points[2].getTime(state) == Approx(0.7f) ); + state.ccEvent(0, 1, 0.0f); + REQUIRE( desc.points[2].getTime(state) == Approx(0.7f) ); + state.ccEvent(0, 1, 0.5f); + REQUIRE( desc.points[2].getTime(state) == Approx(0.35f) ); + state.ccEvent(0, 1, 1.0f); + REQUIRE( desc.points[2].getTime(state) == Approx(0.0f) ); + + REQUIRE( desc.points[2].getLevel(state) == Approx(0.5f) ); + state.ccEvent(0, 2, 0.0f); + REQUIRE( desc.points[2].getLevel(state) == Approx(0.5f) ); + state.ccEvent(0, 2, 0.5f); + REQUIRE( desc.points[2].getLevel(state) == Approx(0.75f) ); + state.ccEvent(0, 2, 1.0f); + REQUIRE( desc.points[2].getLevel(state) == Approx(1.0f) ); +}