diff --git a/src/sfizz/FlexEGDescription.h b/src/sfizz/FlexEGDescription.h index fd8e9428..b8a0f59b 100644 --- a/src/sfizz/FlexEGDescription.h +++ b/src/sfizz/FlexEGDescription.h @@ -34,6 +34,8 @@ struct FlexEGDescription { int dynamic { Default::flexEGDynamic }; // whether parameters can be modulated while EG runs int sustain { Default::flexEGSustain }; // index of the sustain point (default to 0 in ARIA) std::vector points; + // ARIA + bool ampeg = false; // replaces the SFZv1 AmpEG (lowest with this bit wins) }; } // namespace sfz diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 6fd67ac1..3a7402dd 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -104,6 +104,25 @@ void FlexEnvelope::release(unsigned releaseDelay) impl.currentFramesUntilRelease_ = releaseDelay; } +unsigned FlexEnvelope::getRemainingDelay() const noexcept +{ + const Impl& impl = *impl_; + return static_cast(impl.delayFramesLeft_); +} + +bool FlexEnvelope::isReleased() const noexcept +{ + const Impl& impl = *impl_; + return impl.isReleased_; +} + +bool FlexEnvelope::isFinished() const noexcept +{ + const Impl& impl = *impl_; + const FlexEGDescription& desc = *impl.desc_; + return impl.currentStageNumber_ >= desc.points.size(); +} + void FlexEnvelope::process(absl::Span out) { Impl& impl = *impl_; diff --git a/src/sfizz/FlexEnvelope.h b/src/sfizz/FlexEnvelope.h index 3316e715..d1d01a4a 100644 --- a/src/sfizz/FlexEnvelope.h +++ b/src/sfizz/FlexEnvelope.h @@ -40,6 +40,21 @@ public: */ void release(unsigned releaseDelay); + /** + Get the remaining delay samples + */ + unsigned getRemainingDelay() const noexcept; + + /** + Is the envelope released? + */ + bool isReleased() const noexcept; + + /** + Is the envelope finished? + */ + bool isFinished() const noexcept; + /** Process a cycle of the generator. */ diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index 1ffafdbf..ff6cbc6d 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -210,14 +210,18 @@ absl::optional readOpcode(absl::string_view value, const Range readBooleanFromOpcode(const Opcode& opcode) { - switch (hash(opcode.value)) { - case hash("off"): + // Cakewalk-style booleans, case-insensitive + if (absl::EqualsIgnoreCase(opcode.value, "off")) return false; - case hash("on"): + if (absl::EqualsIgnoreCase(opcode.value, "on")) return true; - default: - return {}; - } + + // ARIA-style booleans? (seen in egN_dynamic=1 for example) + // TODO check this + if (auto value = readOpcode(opcode.value, Range::wholeRange())) + return *value != 0; + + return absl::nullopt; } template diff --git a/src/sfizz/Range.h b/src/sfizz/Range.h index 3b548535..7bbe369f 100644 --- a/src/sfizz/Range.h +++ b/src/sfizz/Range.h @@ -8,6 +8,7 @@ #include "MathHelpers.h" #include #include +#include namespace sfz { @@ -116,6 +117,17 @@ public: }; } + /** + * @brief Construct a range which covers the whole numeric domain + */ + static constexpr Range wholeRange() noexcept + { + return Range { + std::numeric_limits::min(), + std::numeric_limits::max(), + }; + } + private: Type _start { static_cast(0.0) }; Type _end { static_cast(0.0) }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2e9a1de5..f98f3ef8 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1162,6 +1162,27 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqBandwidth, Default::eqBandwidthModRange); break; + case hash("eg&_ampeg"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + if (auto ampeg = readBooleanFromOpcode(opcode)) { + FlexEGDescription& desc = flexEGs[egNumber - 1]; + if (desc.ampeg != *ampeg) { + desc.ampeg = *ampeg; + flexAmpEG = absl::nullopt; + for (size_t i = 0, n = flexEGs.size(); i < n && !flexAmpEG; ++i) { + if (flexEGs[i].ampeg) + flexAmpEG = static_cast(i); + } + } + } + break; + } + // Amplitude Envelope case hash("ampeg_attack"): case hash("ampeg_decay"): @@ -1907,7 +1928,7 @@ float sfz::Region::getBendInCents(float bend) const noexcept return bend > 0.0f ? bend * static_cast(bendUp) : -bend * static_cast(bendDown); } -sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +sfz::Region::Connection* sfz::Region::getConnection(const ModKey& source, const ModKey& target) { auto pred = [&source, &target](const Connection& c) { @@ -1915,8 +1936,13 @@ sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source }; auto it = std::find_if(connections.begin(), connections.end(), pred); - if (it != connections.end()) - return *it; + return (it == connections.end()) ? nullptr : &*it; +} + +sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +{ + if (Connection* c = getConnection(source, target)) + return *c; sfz::Region::Connection c; c.source = source; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76840c53..62692437 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -425,6 +425,7 @@ struct Region { // Envelopes std::vector flexEGs; + absl::optional flexAmpEG; // egN_ampeg // LFOs std::vector lfos; @@ -445,6 +446,7 @@ struct Region { float velToDepth = 0.0f; }; std::vector connections; + Connection* getConnection(const ModKey& source, const ModKey& target); Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); // Parent diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 58c360ab..537e7d6a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -169,6 +169,16 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) parseOpcodes(groupOpcodes); parseOpcodes(regionOpcodes); + // Create the amplitude envelope + if (!lastRegion->flexAmpEG) + lastRegion->getOrCreateConnection( + ModKey::createNXYZ(ModId::AmpEG, lastRegion->id), + ModKey::createNXYZ(ModId::MasterAmplitude, lastRegion->id)).sourceDepth = 1.0f; + else + lastRegion->getOrCreateConnection( + ModKey::createNXYZ(ModId::Envelope, lastRegion->id, *lastRegion->flexAmpEG), + ModKey::createNXYZ(ModId::MasterAmplitude, lastRegion->id)).sourceDepth = 1.0f; + if (octaveOffset != 0 || noteOffset != 0) lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); @@ -1522,6 +1532,7 @@ void sfz::Synth::setupModMatrix() case ModId::Envelope: gen = genFlexEnvelope.get(); break; + case ModId::AmpEG: case ModId::PitchEG: case ModId::FilEG: gen = genADSREnvelope.get(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 27323c6d..95921790 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -130,7 +130,6 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event bendStepFactor = centsFactor(region->bendStep); bendSmoother.setSmoothing(region->bendSmooth, sampleRate); bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); - egAmplitude.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); saveModulationTargets(region); @@ -152,10 +151,13 @@ void sfz::Voice::release(int delay) noexcept if (state != State::playing) return; - if (egAmplitude.getRemainingDelay() > delay) { - switchState(State::cleanMeUp); - } else { - egAmplitude.startRelease(delay); + if (!region->flexAmpEG) { + if (egAmplitude.getRemainingDelay() > delay) + switchState(State::cleanMeUp); + } + else { + if (flexEGs[*region->flexAmpEG]->getRemainingDelay() > static_cast(delay)) + switchState(State::cleanMeUp); } resources.modMatrix.releaseVoice(id, region->getId(), delay); @@ -163,10 +165,15 @@ void sfz::Voice::release(int delay) noexcept void sfz::Voice::off(int delay) noexcept { - if (region->offMode == SfzOffMode::fast) { - egAmplitude.setReleaseTime( Default::offTime ); - } else if (region->offMode == SfzOffMode::time) { - egAmplitude.setReleaseTime(region->offTime); + if (!region->flexAmpEG) { + if (region->offMode == SfzOffMode::fast) { + egAmplitude.setReleaseTime( Default::offTime ); + } else if (region->offMode == SfzOffMode::time) { + egAmplitude.setReleaseTime(region->offTime); + } + } + else { + // TODO(jpc): Flex AmpEG } release(delay); @@ -286,8 +293,14 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept panStageMono(buffer); } - if (!egAmplitude.isSmoothing()) - switchState(State::cleanMeUp); + if (!region->flexAmpEG) { + if (!egAmplitude.isSmoothing()) + switchState(State::cleanMeUp); + } + else { + if (flexEGs[*region->flexAmpEG]->isFinished()) + switchState(State::cleanMeUp); + } powerFollower.process(buffer); @@ -367,8 +380,10 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept ModMatrix& mm = resources.modMatrix; - // AmpEG envelope - egAmplitude.getBlock(modulationSpan); + // Amplitude EG + absl::Span ampegOut(mm.getModulation(masterAmplitudeTarget), numSamples); + ASSERT(ampegOut.data()); + copy(ampegOut, modulationSpan); // Amplitude envelope applyGain1(baseGain, modulationSpan); @@ -572,8 +587,14 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept << " for sample " << region->sampleId); } #endif - egAmplitude.setReleaseTime(0.0f); - egAmplitude.startRelease(i); + if (!region->flexAmpEG) { + egAmplitude.setReleaseTime(0.0f); + egAmplitude.startRelease(i); + } + else { + // TODO(jpc): Flex AmpEG + flexEGs[*region->flexAmpEG]->release(i); + } fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -853,7 +874,12 @@ float sfz::Voice::getAveragePower() const noexcept bool sfz::Voice::releasedOrFree() const noexcept { - return state != State::playing || egAmplitude.isReleased(); + if (state != State::playing) + return true; + if (!region->flexAmpEG) + return egAmplitude.isReleased(); + else + return flexEGs[*region->flexAmpEG]->isReleased(); } void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) @@ -1021,6 +1047,7 @@ void sfz::Voice::resetSmoothers() noexcept void sfz::Voice::saveModulationTargets(const Region* region) noexcept { ModMatrix& mm = resources.modMatrix; + masterAmplitudeTarget = mm.findTarget(ModKey::createNXYZ(ModId::MasterAmplitude, region->getId())); amplitudeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Amplitude, region->getId())); volumeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Volume, region->getId())); panTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pan, region->getId())); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 70e5fb74..b70701cf 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -330,6 +330,10 @@ public: Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } + /** + * @brief Get the SFZv1 amplitude EG, if existing + */ + ADSREnvelope* getAmplitudeEG() { return &egAmplitude; } /** * @brief Get the SFZv1 pitch EG, if existing */ @@ -510,6 +514,7 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; + ModMatrix::TargetId masterAmplitudeTarget; ModMatrix::TargetId amplitudeTarget; ModMatrix::TargetId volumeTarget; ModMatrix::TargetId panTarget; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 427d7511..3d837ec5 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -30,12 +30,16 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; case ModId::LFO: return kModIsPerVoice; + case ModId::AmpEG: + return kModIsPerVoice; case ModId::PitchEG: return kModIsPerVoice; case ModId::FilEG: return kModIsPerVoice; // targets + case ModId::MasterAmplitude: + return kModIsPerVoice|kModIsPercentMultiplicative; case ModId::Amplitude: return kModIsPerVoice|kModIsPercentMultiplicative; case ModId::Pan: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index ccd25b98..48d9b2d9 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -23,6 +23,7 @@ enum class ModId : int { Controller = _SourcesStart, Envelope, LFO, + AmpEG, PitchEG, FilEG, @@ -33,7 +34,8 @@ enum class ModId : int { //-------------------------------------------------------------------------- _TargetsStart = _SourcesEnd, - Amplitude = _TargetsStart, + MasterAmplitude = _TargetsStart, + Amplitude, Pan, Width, Position, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 80c3accb..5fa48181 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -74,11 +74,15 @@ std::string ModKey::toString() const return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}"); + case ModId::AmpEG: + return absl::StrCat("AmplitudeEG {", region_.number(), "}"); case ModId::PitchEG: return absl::StrCat("PitchEG {", region_.number(), "}"); case ModId::FilEG: return absl::StrCat("FilterEG {", region_.number(), "}"); + case ModId::MasterAmplitude: + return absl::StrCat("MasterAmplitude {", region_.number(), "}"); case ModId::Amplitude: return absl::StrCat("Amplitude {", region_.number(), "}"); case ModId::Pan: diff --git a/src/sfizz/modulations/sources/ADSREnvelope.cpp b/src/sfizz/modulations/sources/ADSREnvelope.cpp index 7128b216..57f8ba7d 100644 --- a/src/sfizz/modulations/sources/ADSREnvelope.cpp +++ b/src/sfizz/modulations/sources/ADSREnvelope.cpp @@ -35,6 +35,11 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, const EGDescription* desc = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + desc = ®ion->amplitudeEG; + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); @@ -69,6 +74,10 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId voice ADSREnvelope* eg = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); @@ -98,6 +107,10 @@ void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId voic ADSREnvelope* eg = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index 6490e30f..5a8412eb 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -41,7 +41,7 @@ TEST_CASE("[FlexEG] Values") REQUIRE( egDescription.points[4].time == .4_a ); REQUIRE( egDescription.points[4].level == 1.0_a ); REQUIRE( egDescription.sustain == 3 ); - REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + REQUIRE(synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({ R"("EG 1 {0}" -> "Amplitude {0}")", })); } @@ -67,7 +67,7 @@ TEST_CASE("[FlexEG] Default values") REQUIRE( egDescription.points[1].level == 0.0_a ); REQUIRE( egDescription.points[2].time == .1_a ); REQUIRE( egDescription.points[2].level == .25_a ); - REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({}) ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({}) ); } TEST_CASE("[FlexEG] Connections") @@ -85,7 +85,7 @@ TEST_CASE("[FlexEG] Connections") REQUIRE(synth.getNumRegions() == 6); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); - REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({ R"("EG 1 {0}" -> "Amplitude {0}")", R"("EG 1 {1}" -> "Pan {1}")", R"("EG 1 {2}" -> "Width {2}")", diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 05136501..634a92a2 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -92,7 +92,7 @@ width_oncc425=29 )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {0}")", R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {0}")", R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {0}")", @@ -111,7 +111,7 @@ TEST_CASE("[Modulations] Filter CC connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=3}")", R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=2}")", R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=1}")", @@ -129,7 +129,7 @@ TEST_CASE("[Modulations] EQ CC connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=3}")", R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=1}")", R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=2}")", @@ -150,7 +150,7 @@ TEST_CASE("[Modulations] LFO Filter connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("LFO 1 {0}" -> "FilterCutoff {0, N=1}")", R"("LFO 2 {0}" -> "FilterCutoff {0, N=1}")", R"("LFO 3 {0}" -> "FilterResonance {0, N=1}")", @@ -174,7 +174,7 @@ TEST_CASE("[Modulations] EG Filter connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("EG 1 {0}" -> "FilterCutoff {0, N=1}")", R"("EG 2 {0}" -> "FilterCutoff {0, N=1}")", R"("EG 3 {0}" -> "FilterResonance {0, N=1}")", @@ -198,7 +198,7 @@ TEST_CASE("[Modulations] LFO EQ connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("LFO 1 {0}" -> "EqBandwidth {0, N=1}")", R"("LFO 2 {0}" -> "EqFrequency {0, N=2}")", R"("LFO 3 {0}" -> "EqGain {0, N=3}")", @@ -222,7 +222,7 @@ TEST_CASE("[Modulations] EG EQ connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("EG 1 {0}" -> "EqBandwidth {0, N=1}")", R"("EG 2 {0}" -> "EqFrequency {0, N=2}")", R"("EG 3 {0}" -> "EqGain {0, N=3}")", @@ -231,3 +231,76 @@ TEST_CASE("[Modulations] EG EQ connections") R"("EG 6 {0}" -> "EqFrequency {0, N=1}")", })); } + + +TEST_CASE("[Modulations] FlexEG Ampeg target") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg1_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 1 {0}" -> "MasterAmplitude {0}")", + })); +} + +TEST_CASE("[Modulations] FlexEG Ampeg target with 2 FlexEGs") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg2_time1=0 eg2_level1=1 + eg2_time2=1 eg2_level2=0 + eg2_time3=1 eg2_level3=.5 eg1_sustain=3 + eg2_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 2 {0}" -> "MasterAmplitude {0}")", + })); +} + + +TEST_CASE("[Modulations] FlexEG Ampeg target with multiple EGs targeting ampeg") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg1_ampeg=1 + eg2_time1=0 eg2_level1=1 + eg2_time2=1 eg2_level2=0 + eg2_time3=1 eg2_level3=.5 eg1_sustain=3 + eg2_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 1 {0}" -> "MasterAmplitude {0}")", + })); +} + diff --git a/tests/OpcodeT.cpp b/tests/OpcodeT.cpp index 495a4f5f..336907f7 100644 --- a/tests/OpcodeT.cpp +++ b/tests/OpcodeT.cpp @@ -285,3 +285,14 @@ TEST_CASE("[Opcode] readOpcode") REQUIRE( !sfz::readOpcode("garbage50.25", sfz::Range(-20, 100)) ); REQUIRE( !sfz::readOpcode("garbage", sfz::Range(-20, 100)) ); } + +TEST_CASE("[Opcode] readBooleanFromOpcode") +{ + REQUIRE(sfz::readBooleanFromOpcode({"", "1"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "0"}) == false); + REQUIRE(sfz::readBooleanFromOpcode({"", "777"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "on"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "off"}) == false); + REQUIRE(sfz::readBooleanFromOpcode({"", "On"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "oFf"}) == false); +} diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index b3521c4a..300e1cef 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -69,9 +69,12 @@ unsigned numPlayingVoices(const sfz::Synth& synth) }); } -std::string createReferenceGraph(std::vector lines, int numRegions) +std::string createDefaultGraph(std::vector lines, int numRegions) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { + lines.push_back(absl::StrCat( + R"("AmplitudeEG {)", regionIdx, R"(}" -> "MasterAmplitude {)", regionIdx, R"(}")" + )); lines.push_back(absl::StrCat( R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {)", regionIdx, @@ -84,6 +87,11 @@ std::string createReferenceGraph(std::vector lines, int numRegions) )); } + return createModulationDotGraph(lines); +}; + +std::string createModulationDotGraph(std::vector lines) +{ std::sort(lines.begin(), lines.end()); std::string graph; @@ -98,4 +106,4 @@ std::string createReferenceGraph(std::vector lines, int numRegions) graph += "}\n"; return graph; -}; +} diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index 272fb612..efc5d6d3 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -67,10 +67,17 @@ const std::vector getPlayingVoices(const sfz::Synth& synth); unsigned numPlayingVoices(const sfz::Synth& synth); /** - * @brief Create the dot graph representation from a list of strings + * @brief Create the default dot graph representation for standard regions * */ -std::string createReferenceGraph(std::vector lines, int numRegions = 1); +std::string createDefaultGraph(std::vector lines, int numRegions = 1); + +/** + * @brief Create a dot graph with the specified lines. + * The lines are sorted. + * + */ +std::string createModulationDotGraph(std::vector lines); template inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3)