From b692377f7b0cbdd5adfcd76ca72c452f5ffa3340 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 20 Aug 2020 23:37:23 +0200 Subject: [PATCH 1/9] Add the Flex EG --- dpf.mk | 3 + src/CMakeLists.txt | 6 + src/sfizz/Curve.cpp | 7 + src/sfizz/Curve.h | 10 +- src/sfizz/Defaults.h | 14 ++ src/sfizz/FlexEGDescription.cpp | 87 ++++++++ src/sfizz/FlexEGDescription.h | 39 ++++ src/sfizz/FlexEnvelope.cpp | 206 ++++++++++++++++++ src/sfizz/FlexEnvelope.h | 53 +++++ src/sfizz/Region.cpp | 141 ++++++++++++ src/sfizz/Region.h | 4 + src/sfizz/Synth.cpp | 11 + src/sfizz/Synth.h | 3 + src/sfizz/Voice.cpp | 12 + src/sfizz/Voice.h | 14 ++ .../modulations/sources/FlexEnvelope.cpp | 86 ++++++++ src/sfizz/modulations/sources/FlexEnvelope.h | 24 ++ 17 files changed, 717 insertions(+), 3 deletions(-) create mode 100644 src/sfizz/FlexEGDescription.cpp create mode 100644 src/sfizz/FlexEGDescription.h create mode 100644 src/sfizz/FlexEnvelope.cpp create mode 100644 src/sfizz/FlexEnvelope.h create mode 100644 src/sfizz/modulations/sources/FlexEnvelope.cpp create mode 100644 src/sfizz/modulations/sources/FlexEnvelope.h diff --git a/dpf.mk b/dpf.mk index 2ee507e8..1c8159fa 100644 --- a/dpf.mk +++ b/dpf.mk @@ -66,6 +66,8 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/Controller.cpp \ + src/sfizz/modulations/sources/FlexEGDescription.cpp \ + src/sfizz/modulations/sources/FlexEnvelope.cpp \ src/sfizz/modulations/sources/LFO.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ @@ -91,6 +93,7 @@ SFIZZ_SOURCES = \ src/sfizz/FileMetadata.cpp \ src/sfizz/FilePool.cpp \ src/sfizz/FilterPool.cpp \ + src/sfizz/FlexEnvelope.cpp \ src/sfizz/FloatEnvelopes.cpp \ src/sfizz/Logger.cpp \ src/sfizz/LFO.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6083c110..4adbe3fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set (SFIZZ_HEADERS sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h sfizz/modulations/sources/Controller.h + sfizz/modulations/sources/FlexEnvelope.h sfizz/modulations/sources/LFO.h sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h @@ -67,6 +68,8 @@ set (SFIZZ_HEADERS sfizz/FilePool.h sfizz/FilterDescription.h sfizz/FilterPool.h + sfizz/FlexEGDescription.h + sfizz/FlexEnvelope.h sfizz/HistoricalBuffer.h sfizz/Interpolators.h sfizz/Interpolators.hpp @@ -139,11 +142,14 @@ set (SFIZZ_SOURCES sfizz/LFO.cpp sfizz/LFODescription.cpp sfizz/PowerFollower.cpp + sfizz/FlexEGDescription.cpp + sfizz/FlexEnvelope.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp + sfizz/modulations/sources/FlexEnvelope.cpp sfizz/modulations/sources/LFO.cpp sfizz/utility/SpinMutex.cpp sfizz/effects/Nothing.cpp diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 89320db5..8d204a39 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -147,6 +147,13 @@ Curve Curve::buildBipolar(float v1, float v2) return curve; } +Curve Curve::buildFromPoints(const float points[NumValues]) +{ + Curve curve; + copy(absl::MakeConstSpan(points, NumValues), absl::Span(curve._points)); + return curve; +} + const Curve& Curve::getDefault() { return defaultCurve; diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h index 3cf4b2cb..5c0ed361 100644 --- a/src/sfizz/Curve.h +++ b/src/sfizz/Curve.h @@ -21,6 +21,8 @@ struct Opcode; */ class Curve { public: + enum { NumValues = 128 }; + /** * @brief Compute the curve for integral x in domain [0:127] */ @@ -93,14 +95,16 @@ public: */ static Curve buildBipolar(float v1, float v2); + /** + * @brief Build a curve from a table of points + */ + static Curve buildFromPoints(const float points[NumValues]); + /** * @brief Get a linear curve from 0 to 1 */ static const Curve& getDefault(); -private: - enum { NumValues = 128 }; - private: void fill(Interpolator itp, const bool fillStatus[NumValues]); void lerpFill(const bool fillStatus[NumValues]); diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index d53f3772..b12d1b8a 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -244,6 +244,20 @@ namespace Default constexpr Range egOnCCTimeRange { -100.0, 100.0 }; constexpr Range egOnCCPercentRange { -100.0, 100.0 }; + // Flex envelope generators + constexpr int numFlexEGs { 4 }; + constexpr int numFlexEGPoints { 8 }; + constexpr int flexEGDynamic { 0 }; + constexpr int flexEGSustain { 0 }; + constexpr float flexEGPointTime { 0 }; + constexpr float flexEGPointLevel { 0 }; + constexpr float flexEGPointShape { 0 }; + constexpr Range flexEGDynamicRange { 0, 1 }; + constexpr Range flexEGSustainRange { 0, 100 }; + constexpr Range flexEGPointTimeRange { 0.0f, 100.0f }; + constexpr Range flexEGPointLevelRange { -1.0f, 1.0f }; + constexpr Range flexEGPointShapeRange { -100.0f, 100.0f }; + // ***** SFZ v2 ******** constexpr int sampleQuality { 1 }; constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive diff --git a/src/sfizz/FlexEGDescription.cpp b/src/sfizz/FlexEGDescription.cpp new file mode 100644 index 00000000..18ea92e3 --- /dev/null +++ b/src/sfizz/FlexEGDescription.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FlexEGDescription.h" +#include "Curve.h" +#include +#include + +namespace sfz { + +void FlexEGPoint::setShape(float shape) +{ + shape_ = shape; + shapeCurve_ = FlexEGs::getShapeCurve(shape); +} + +const Curve& FlexEGPoint::curve() const +{ + if (shapeCurve_) + return *shapeCurve_; + else + return Curve::getDefault(); +} + +/// +typedef absl::flat_hash_map> FlexEGShapes; + +static FlexEGShapes& getShapeMap() +{ + static FlexEGShapes shapes; + return shapes; +} + +std::shared_ptr FlexEGs::getShapeCurve(float shape) +{ + static FlexEGShapes& map = getShapeMap(); + + std::weak_ptr& slot = map[shape]; + + std::shared_ptr curve = slot.lock(); + if (curve) + return curve; + + curve.reset(new Curve); + + /// + constexpr unsigned numPoints = Curve::NumValues; + float points[numPoints]; + + if (shape == 0) + *curve = Curve::getDefault(); + else if (shape > 0) { + for (unsigned i = 0; i < numPoints; ++i) { + float x = float(i) / (numPoints - 1); + points[i] = std::pow(x, shape); + } + *curve = Curve::buildFromPoints(points); + } + else if (shape < 0) { + for (unsigned i = 0; i < numPoints; ++i) { + float x = float(i) / (numPoints - 1); + points[i] = 1 - std::pow(1 - x, -shape); + } + *curve = Curve::buildFromPoints(points); + } + + /// + slot = curve; + return curve; +} + +void FlexEGs::clearUnusedCurves() +{ + static FlexEGShapes& map = getShapeMap(); + + for (auto it = map.begin(); it != map.end(); ) { + if (it->second.use_count() == 0) + map.erase(it++); + else + ++it; + } +} + +} // namespace sfz diff --git a/src/sfizz/FlexEGDescription.h b/src/sfizz/FlexEGDescription.h new file mode 100644 index 00000000..fd8e9428 --- /dev/null +++ b/src/sfizz/FlexEGDescription.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Defaults.h" +#include +#include + +namespace sfz { +class Curve; + +namespace FlexEGs { + std::shared_ptr getShapeCurve(float shape); + void clearUnusedCurves(); +}; + +struct FlexEGPoint { + float time { Default::flexEGPointTime }; // duration until next step (s) + float level { Default::flexEGPointLevel }; // normalized amplitude + + void setShape(float shape); + float shape() const noexcept { return shape_; } + const Curve& curve() const; + +private: + float shape_ { Default::flexEGPointShape }; // 0: linear, positive: exp, negative: log + std::shared_ptr shapeCurve_; +}; + +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; +}; + +} // namespace sfz diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp new file mode 100644 index 00000000..3810ec73 --- /dev/null +++ b/src/sfizz/FlexEnvelope.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/* + Note(jpc): implementation status + +- [ ] egN_points (purpose unknown) +- [x] egN_timeX +- [x] egN_levelX +- [x] egN_shapeX +- [x] egN_sustain +- [ ] egN_dynamic +*/ + +#include "FlexEnvelope.h" +#include "FlexEGDescription.h" +#include "Curve.h" +#include "Config.h" +#include "SIMDHelpers.h" +#include + +namespace sfz { + +struct FlexEnvelope::Impl { + const FlexEGDescription* desc_ { nullptr }; + float samplePeriod_ { 1.0 / config::defaultSampleRate }; + size_t delayFramesLeft_ { 0 }; + + // + float stageSourceLevel_ { 0.0 }; + float stageTargetLevel_ { 0.0 }; + float stageTime_ { 0.0 }; + bool stageSustained_ { false }; + const Curve* stageCurve_ { nullptr }; + + // + unsigned currentStageNumber_ { 0 }; + float currentLevel_ { 0.0 }; + float currentTime_ { 0.0 }; + absl::optional currentFramesUntilRelease_ { absl::nullopt }; + bool isReleased_ { false }; + + // + void process(absl::Span out); + bool advanceToNextStage(); +}; + +FlexEnvelope::FlexEnvelope() + : impl_(new Impl) +{ +} + +FlexEnvelope::~FlexEnvelope() +{ +} + +void FlexEnvelope::setSampleRate(double sampleRate) +{ + Impl& impl = *impl_; + impl.samplePeriod_ = 1.0 / sampleRate; +} + +void FlexEnvelope::configure(const FlexEGDescription* desc) +{ + Impl& impl = *impl_; + impl.desc_ = desc; +} + +void FlexEnvelope::start(unsigned triggerDelay) +{ + Impl& impl = *impl_; + const FlexEGDescription& desc = *impl.desc_; + + impl.delayFramesLeft_ = triggerDelay; + + FlexEGPoint point; + if (!desc.points.empty()) + point = desc.points[0]; + + // + impl.stageSourceLevel_ = 0.0; + impl.stageTargetLevel_ = point.level; + impl.stageTime_ = point.time; + impl.stageSustained_ = desc.sustain == 0; + impl.stageCurve_ = &point.curve(); + impl.currentFramesUntilRelease_ = absl::nullopt; + impl.isReleased_ = false; + + // + impl.currentStageNumber_ = 0; + impl.currentLevel_ = 0.0; + impl.currentTime_ = 0.0; +} + +void FlexEnvelope::release(unsigned releaseDelay) +{ + Impl& impl = *impl_; + impl.currentFramesUntilRelease_ = releaseDelay; +} + +void FlexEnvelope::process(absl::Span out) +{ + Impl& impl = *impl_; + impl.process(out); +} + +void FlexEnvelope::Impl::process(absl::Span out) +{ + const FlexEGDescription& desc = *desc_; + size_t numFrames = out.size(); + const float samplePeriod = samplePeriod_; + + // Skip the initial delay, for frame-accurate trigger + size_t skipFrames = std::min(numFrames, delayFramesLeft_); + if (skipFrames > 0) { + delayFramesLeft_ -= skipFrames; + fill(absl::MakeSpan(out.data(), skipFrames), 0.0f); + out.remove_prefix(skipFrames); + numFrames -= skipFrames; + } + + // Envelope finished? + if (currentStageNumber_ >= desc.points.size()) { + fill(out, 0.0f); + return; + } + + size_t frameIndex = 0; + absl::optional framesUntilRelease = currentFramesUntilRelease_; + + while (frameIndex < numFrames) { + // Check for release + if (framesUntilRelease && *framesUntilRelease == 0) { + isReleased_ = true; + framesUntilRelease = absl::nullopt; + } + + // Perform stage transitions + const bool isReleased = isReleased_; + while ((!stageSustained_ && currentTime_ >= stageTime_) || + (stageSustained_ && isReleased)) { + if (!advanceToNextStage()) { + fill(out, 0.0f); + return; + } + } + + // Process without going past the release point, if there is one + size_t maxFrameIndex = numFrames; + if (framesUntilRelease) + maxFrameIndex = std::min(maxFrameIndex, frameIndex + *framesUntilRelease); + + // Process the current stage + float time = currentTime_; + float level = currentLevel_; + const float stageEndTime = stageTime_; + const float sourceLevel = stageSourceLevel_; + const float targetLevel = stageTargetLevel_; + const bool sustained = stageSustained_; + const Curve& curve = *stageCurve_; + size_t framesDone = 0; + while ((time < stageEndTime || sustained) && frameIndex < maxFrameIndex) { + float x = time * (1.0f / stageEndTime); + float c = curve.evalNormalized(x); + level = sourceLevel + c * (targetLevel - sourceLevel); + out[frameIndex++] = level; + time += samplePeriod; + ++framesDone; + } + currentLevel_ = level; + + // Update the counter to release + if (framesUntilRelease) + *framesUntilRelease -= framesDone; + + currentTime_ = time; + } + + currentFramesUntilRelease_ = framesUntilRelease; +} + +bool FlexEnvelope::Impl::advanceToNextStage() +{ + const FlexEGDescription& desc = *desc_; + + unsigned nextStageNo = currentStageNumber_ + 1; + currentStageNumber_ = nextStageNo; + + if (currentStageNumber_ >= desc.points.size()) + return false; + + const FlexEGPoint& point = desc.points[currentStageNumber_]; + stageSourceLevel_ = currentLevel_; + stageTargetLevel_ = point.level; + stageTime_ = point.time; + stageSustained_ = int(nextStageNo) == desc.sustain; + stageCurve_ = &point.curve(); + + currentTime_ = 0; + return true; +}; + +} // namespace sfz diff --git a/src/sfizz/FlexEnvelope.h b/src/sfizz/FlexEnvelope.h new file mode 100644 index 00000000..3316e715 --- /dev/null +++ b/src/sfizz/FlexEnvelope.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { +struct FlexEGDescription; + +/** + Flex envelope generator (according to ARIA) + */ +class FlexEnvelope { +public: + FlexEnvelope(); + ~FlexEnvelope(); + + /** + Sets the sample rate. + */ + void setSampleRate(double sampleRate); + + /** + Attach some control parameters to this EG. + The control structure is owned by the caller. + */ + void configure(const FlexEGDescription* desc); + + /** + Start processing an EG as a region is triggered. + */ + void start(unsigned triggerDelay); + + /** + Release the EG. + */ + void release(unsigned releaseDelay); + + /** + Process a cycle of the generator. + */ + void process(absl::Span out); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b64af303..a6cea577 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1046,6 +1046,80 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } break; + // Modulation: Flex EG (targets) + case hash("eg&_amplitude"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_pan"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::panCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pan, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_width"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::widthCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Width, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_position"): // sfizz extension + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::positionCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Position, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_pitch"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pitch, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_volume"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Volume, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + // Amplitude Envelope case hash("ampeg_attack"): setValueFromOpcode(opcode, amplitudeEG.attack, Default::egTimeRange); @@ -1155,6 +1229,73 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; + // Flex envelopes + case hash("eg&_dynamic"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + setValueFromOpcode(opcode, eg.dynamic, Default::flexEGDynamicRange); + } + break; + case hash("eg&_sustain"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + setValueFromOpcode(opcode, eg.sustain, Default::flexEGSustainRange); + } + break; + case hash("eg&_time&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + setValueFromOpcode(opcode, eg.points[pointNumber].time, Default::flexEGPointTimeRange); + } + break; + case hash("eg&_level&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + setValueFromOpcode(opcode, eg.points[pointNumber].level, Default::flexEGPointLevelRange); + } + break; + case hash("eg&_shape&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + if (auto value = readOpcode(opcode.value, Default::flexEGPointShapeRange)) + eg.points[pointNumber].setShape(*value); + } + break; + case hash("effect&"): { const auto effectNumber = opcode.parameters.back(); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4635f78c..4c9dabba 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -10,6 +10,7 @@ #include "LeakDetector.h" #include "Defaults.h" #include "EGDescription.h" +#include "FlexEGDescription.h" #include "EQDescription.h" #include "FilterDescription.h" #include "LFODescription.h" @@ -379,6 +380,9 @@ struct Region { EGDescription pitchEG; EGDescription filterEG; + // Envelopes + std::vector flexEGs; + // LFOs std::vector lfos; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index beae4e64..e69cd215 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -18,6 +18,7 @@ #include "modulations/ModId.h" #include "modulations/sources/Controller.h" #include "modulations/sources/LFO.h" +#include "modulations/sources/FlexEnvelope.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -46,6 +47,7 @@ sfz::Synth::Synth(int numVoices) // modulation sources genController.reset(new ControllerSource(resources)); genLFO.reset(new LFOSource(*this)); + genFlexEnvelope.reset(new FlexEnvelopeSource(*this)); } sfz::Synth::~Synth() @@ -480,6 +482,9 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; + + FlexEGs::clearUnusedCurves(); while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -595,6 +600,7 @@ void sfz::Synth::finalizeSfzLoad() maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); maxLFOs = max(maxLFOs, region->lfos.size()); + maxFlexEGs = max(maxFlexEGs, region->flexEGs.size()); ++currentRegionIndex; } @@ -605,6 +611,7 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; settingsPerVoice.maxLFOs = maxLFOs; + settingsPerVoice.maxFlexEGs = maxFlexEGs; applySettingsPerVoice(); @@ -1473,6 +1480,7 @@ void sfz::Synth::applySettingsPerVoice() voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); + voice->setMaxFlexEGsPerVoice(settingsPerVoice.maxFlexEGs); } } @@ -1491,6 +1499,9 @@ void sfz::Synth::setupModMatrix() case ModId::LFO: gen = genLFO.get(); break; + case ModId::Envelope: + gen = genFlexEnvelope.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 995bb5e6..fd019dc3 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -28,6 +28,7 @@ namespace sfz { class ControllerSource; class LFOSource; +class FlexEnvelopeSource; /** * @brief This class is the core of the sfizz library. In C++ it is the main point @@ -897,12 +898,14 @@ private: // Modulation source generators std::unique_ptr genController; std::unique_ptr genLFO; + std::unique_ptr genFlexEnvelope; // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b19c6cb5..cb604633 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -13,6 +13,7 @@ #include "Panning.h" #include "SfzHelpers.h" #include "LFO.h" +#include "FlexEnvelope.h" #include "modulations/ModId.h" #include "modulations/ModKey.h" #include "modulations/ModMatrix.h" @@ -798,6 +799,17 @@ void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs) } } +void sfz::Voice::setMaxFlexEGsPerVoice(size_t numFlexEGs) +{ + flexEGs.resize(numFlexEGs); + + for (size_t i = 0; i < numFlexEGs; ++i) { + auto eg = absl::make_unique(); + eg->setSampleRate(sampleRate); + flexEGs[i] = std::move(eg); + } +} + void sfz::Voice::setupOscillatorUnison() { int m = region->oscillatorMulti; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 10e0df2a..8833271a 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -25,6 +25,7 @@ namespace sfz { enum InterpolatorModel : int; class LFO; +class FlexEnvelope; /** * @brief The SFZ voice are the polyphony holders. They get activated by the synth * and tasked to play a given region until the end, stopping on note-offs, off-groups @@ -263,6 +264,12 @@ public: * @param index */ LFO* getLFO(size_t index) { return lfos[index].get(); } + /** + * @brief Get the Flex EG designated by the given index + * + * @param index + */ + FlexEnvelope* getFlexEG(size_t index) { return flexEGs[index].get(); } /** * @brief Set the max number of filters per voice * @@ -281,6 +288,12 @@ public: * @param numLFOs */ void setMaxLFOsPerVoice(size_t numLFOs); + /** + * @brief Set the max number of Flex EGs per voice + * + * @param numFlexEGs + */ + void setMaxFlexEGsPerVoice(size_t numFlexEGs); /** * @brief Release the voice after a given delay * @@ -444,6 +457,7 @@ private: std::vector filters; std::vector equalizers; std::vector> lfos; + std::vector> flexEGs; ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; diff --git a/src/sfizz/modulations/sources/FlexEnvelope.cpp b/src/sfizz/modulations/sources/FlexEnvelope.cpp new file mode 100644 index 00000000..557dd4db --- /dev/null +++ b/src/sfizz/modulations/sources/FlexEnvelope.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FlexEnvelope.h" +#include "../../FlexEnvelope.h" +#include "../../Synth.h" +#include "../../Voice.h" +#include "../../SIMDHelpers.h" +#include "../../Config.h" +#include "../../Debug.h" + +namespace sfz { + +FlexEnvelopeSource::FlexEnvelopeSource(Synth &synth) + : synth_(&synth) +{ +} + +void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->configure(®ion->flexEGs[egIndex]); + eg->start(delay); +} + +void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->release(delay); +} + +void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->process(buffer); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/FlexEnvelope.h b/src/sfizz/modulations/sources/FlexEnvelope.h new file mode 100644 index 00000000..c4e50fc4 --- /dev/null +++ b/src/sfizz/modulations/sources/FlexEnvelope.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" + +namespace sfz { +class Synth; + +class FlexEnvelopeSource : public ModGenerator { +public: + explicit FlexEnvelopeSource(Synth &synth); + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz From 84bb5532926c63415b373cdcf6566729d100b9ea Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 22 Aug 2020 01:03:10 +0200 Subject: [PATCH 2/9] Some changes of little impact --- src/sfizz/FlexEnvelope.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 3810ec73..c3e09359 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -189,10 +189,10 @@ bool FlexEnvelope::Impl::advanceToNextStage() unsigned nextStageNo = currentStageNumber_ + 1; currentStageNumber_ = nextStageNo; - if (currentStageNumber_ >= desc.points.size()) + if (nextStageNo >= desc.points.size()) return false; - const FlexEGPoint& point = desc.points[currentStageNumber_]; + const FlexEGPoint& point = desc.points[nextStageNo]; stageSourceLevel_ = currentLevel_; stageTargetLevel_ = point.level; stageTime_ = point.time; From d2f77f63dc1199280c3a4ae1dff853eff7a40f62 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 22 Aug 2020 01:11:17 +0200 Subject: [PATCH 3/9] Ensure to update all variables on early return --- src/sfizz/FlexEnvelope.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index c3e09359..abc2b4a0 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -129,13 +129,12 @@ void FlexEnvelope::Impl::process(absl::Span out) } size_t frameIndex = 0; - absl::optional framesUntilRelease = currentFramesUntilRelease_; while (frameIndex < numFrames) { // Check for release - if (framesUntilRelease && *framesUntilRelease == 0) { + if (currentFramesUntilRelease_ && *currentFramesUntilRelease_ == 0) { isReleased_ = true; - framesUntilRelease = absl::nullopt; + currentFramesUntilRelease_ = absl::nullopt; } // Perform stage transitions @@ -150,8 +149,8 @@ void FlexEnvelope::Impl::process(absl::Span out) // Process without going past the release point, if there is one size_t maxFrameIndex = numFrames; - if (framesUntilRelease) - maxFrameIndex = std::min(maxFrameIndex, frameIndex + *framesUntilRelease); + if (currentFramesUntilRelease_) + maxFrameIndex = std::min(maxFrameIndex, frameIndex + *currentFramesUntilRelease_); // Process the current stage float time = currentTime_; @@ -173,13 +172,11 @@ void FlexEnvelope::Impl::process(absl::Span out) currentLevel_ = level; // Update the counter to release - if (framesUntilRelease) - *framesUntilRelease -= framesDone; + if (currentFramesUntilRelease_) + *currentFramesUntilRelease_ -= framesDone; currentTime_ = time; } - - currentFramesUntilRelease_ = framesUntilRelease; } bool FlexEnvelope::Impl::advanceToNextStage() From 62ee1abd241faa0555d35e6bd2bff69892b02e0b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 12 Sep 2020 02:03:07 +0200 Subject: [PATCH 4/9] Comment on unimplemented loop opcodes --- src/sfizz/FlexEnvelope.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index abc2b4a0..321093fc 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -13,6 +13,9 @@ - [x] egN_shapeX - [x] egN_sustain - [ ] egN_dynamic +- [ ] egN_loop +- [ ] egN_loop_shape +- [ ] egN_loop_count */ #include "FlexEnvelope.h" From 19540e821aca42ff3325ef979c0e0a21ee41f8d8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 15 Sep 2020 22:23:42 +0200 Subject: [PATCH 5/9] Add a circular test case for the curve --- tests/CurveT.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 4c3c9f9d..68a88b9c 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -9,6 +9,7 @@ #include "catch2/catch.hpp" #include using namespace Catch::literals; +using namespace sfz::literals; TEST_CASE("[Curve] Bipolar 0 to 1") { @@ -242,3 +243,20 @@ TEST_CASE("[Curve] Default CurveSet") REQUIRE( curveSet.getCurve(6).evalNormalized(1.0f) == 0.0f ); REQUIRE( curveSet.getCurve(6).evalNormalized(0.3f) == Approx(0.837).margin(1e-3) ); } + +TEST_CASE("[Curve] Build from points") +{ + std::array curvePoints; + float val = 0.0f; + float step = 1 / static_cast(sfz::Curve::NumValues); + for (auto& x : curvePoints) { + x = val; + val += step; + } + + sfz::Curve curve = sfz::Curve::buildFromPoints(curvePoints.data()); + REQUIRE(curve.evalNormalized(0.0) == curvePoints[0]); + REQUIRE(curve.evalNormalized(1.0) == curvePoints[sfz::Curve::NumValues - 1]); + REQUIRE(curve.evalNormalized(63_norm) == curvePoints[63]); +} + From 4d87716a305e39723836553a81efade0b1d64e93 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 08:41:02 +0200 Subject: [PATCH 6/9] Offset the time so that the envelopes "end" on the correct value The envelope start is always considered to be the value before the current ramp started --- src/sfizz/FlexEnvelope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 321093fc..d56aad25 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -165,11 +165,11 @@ void FlexEnvelope::Impl::process(absl::Span out) const Curve& curve = *stageCurve_; size_t framesDone = 0; while ((time < stageEndTime || sustained) && frameIndex < maxFrameIndex) { + time += samplePeriod; float x = time * (1.0f / stageEndTime); float c = curve.evalNormalized(x); level = sourceLevel + c * (targetLevel - sourceLevel); out[frameIndex++] = level; - time += samplePeriod; ++framesDone; } currentLevel_ = level; From f969761b5abc5263b226fe8ab25f758641ec7fa5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 08:45:29 +0200 Subject: [PATCH 7/9] Only zero the remaining samples when the envelope is finished --- src/sfizz/FlexEnvelope.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index d56aad25..70dc0948 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -145,6 +145,7 @@ void FlexEnvelope::Impl::process(absl::Span out) while ((!stageSustained_ && currentTime_ >= stageTime_) || (stageSustained_ && isReleased)) { if (!advanceToNextStage()) { + out.remove_prefix(frameIndex); fill(out, 0.0f); return; } From f317e2f6b9679d5fa23c3ec44e3979ac5218b7e6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 09:03:39 +0200 Subject: [PATCH 8/9] Add flex eg tests, move test helpers around, and add a more verbose mod matrix graph output --- src/sfizz/modulations/ModKey.cpp | 16 +- tests/ADSREnvelopeT.cpp | 16 +- tests/CMakeLists.txt | 1 + tests/FlexEGT.cpp | 263 +++++++++++++++++++++++++++++++ tests/ModulationsT.cpp | 35 +--- tests/TestHelpers.cpp | 31 ++++ tests/TestHelpers.h | 21 +++ 7 files changed, 330 insertions(+), 53 deletions(-) create mode 100644 tests/FlexEGT.cpp diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index c5ee2a37..9012fb46 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -71,22 +71,22 @@ std::string ModKey::toString() const " {curve=", params_.curve, ", smooth=", params_.smooth, ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: - return absl::StrCat("EG ", 1 + params_.N); + return absl::StrCat("EG ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::LFO: - return absl::StrCat("LFO ", 1 + params_.N); + return absl::StrCat("LFO ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::Amplitude: - return "Amplitude"; + return absl::StrCat("Amplitude", " {region=", region_.number(), "}"); case ModId::Pan: - return "Pan"; + return absl::StrCat("Pan", " {region=", region_.number(), "}"); case ModId::Width: - return "Width"; + return absl::StrCat("Width", " {region=", region_.number(), "}"); case ModId::Position: - return "Position"; + return absl::StrCat("Position", " {region=", region_.number(), "}"); case ModId::Pitch: - return "Pitch"; + return absl::StrCat("Pitch", " {region=", region_.number(), "}"); case ModId::Volume: - return "Volume"; + return absl::StrCat("Volume", " {region=", region_.number(), "}"); default: return {}; diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index df104407..424d8273 100644 --- a/tests/ADSREnvelopeT.cpp +++ b/tests/ADSREnvelopeT.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/ADSREnvelope.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" #include #include @@ -13,21 +14,6 @@ #include using namespace Catch::literals; -template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - TEST_CASE("[ADSREnvelope] Basic state") { sfz::ADSREnvelope envelope; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61b16360..0090620c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(SFIZZ_TEST_SOURCES # If we're tweaking the curves this kind of tests does not make sense # Use integration tests with comparison curves # ADSREnvelopeT.cpp + FlexEGT.cpp EventEnvelopesT.cpp MainT.cpp SynthT.cpp diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp new file mode 100644 index 00000000..f48e5455 --- /dev/null +++ b/tests/FlexEGT.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + + +#include "sfizz/Synth.h" +#include "sfizz/FlexEnvelope.h" +#include "catch2/catch.hpp" +#include "TestHelpers.h" +using namespace Catch::literals; +using namespace sfz::literals; + +TEST_CASE("[FlexEG] Values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_amplitude=1 + eg1_time1=.1 eg1_level1=.25 + eg1_time2=.2 eg1_level2=1 + eg1_time3=.2 eg1_level3=.5 eg1_sustain=3 + eg1_time4=.4 eg1_level4=1 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 1 ); + const auto& egDescription = region->flexEGs[0]; + REQUIRE( egDescription.points.size() == 5 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == .1_a ); + REQUIRE( egDescription.points[1].level == .25_a ); + REQUIRE( egDescription.points[2].time == .2_a ); + REQUIRE( egDescription.points[2].level == 1.0_a ); + REQUIRE( egDescription.points[3].time == .2_a ); + REQUIRE( egDescription.points[3].level == .5_a ); + 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({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + })); +} + +TEST_CASE("[FlexEG] Default values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg3_time2=.1 eg3_level2=.25 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 3 ); + REQUIRE( region->flexEGs[0].points.size() == 0 ); + REQUIRE( region->flexEGs[1].points.size() == 0 ); + const auto& egDescription = region->flexEGs[2]; + REQUIRE( egDescription.points.size() == 3 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == 0.0_a ); + 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({}) ); +} + +TEST_CASE("[FlexEG] Connections") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine eg1_amplitude=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pan=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_width=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_position=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pitch=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_volume=1 eg1_time1=.1 eg1_level1=.25 + )"); + 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({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + R"("EG 1 {region=1}" -> "Pan {region=1}")", + R"("EG 1 {region=2}" -> "Width {region=2}")", + R"("EG 1 {region=3}" -> "Position {region=3}")", + R"("EG 1 {region=4}" -> "Pitch {region=4}")", + R"("EG 1 {region=5}" -> "Volume {region=5}")", + }, 6)); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (No release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(16); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { 0.0f, 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, 1.0f, 1.0f }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[16] == 0.0_a ); // released + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with release and release ramp)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_time3=0.5 eg1_level3=0 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, + 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.8f, 0.6f, 0.4f, 0.2f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_sustain=2 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.01f, 0.04f, 0.09f, 0.16f, 0.25f, + 0.58f, 0.72f, 0.83f, 0.92f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.99f, 0.97f, 0.87f, 0.59f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected, 0.01f) ); +} diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 48203a2e..627714d6 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -7,6 +7,7 @@ #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" #include "sfizz/Synth.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" TEST_CASE("[Modulations] Identifiers") @@ -78,32 +79,6 @@ TEST_CASE("[Modulations] Display names") }); } -static std::string createReferenceGraph(std::vector lines) -{ - const char* defaultConnections[] = { - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude")", - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan")" - }; - - for (const char* line : defaultConnections) - lines.push_back(line); - - std::sort(lines.begin(), lines.end()); - - std::string graph; - graph.reserve(1024); - - graph += "digraph {\n"; - for (const std::string& line : lines) { - graph.push_back('\t'); - graph += line; - graph.push_back('\n'); - } - graph += "}\n"; - - return graph; -}; - TEST_CASE("[Modulations] Connection graph from SFZ") { sfz::Synth synth; @@ -118,9 +93,9 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width")", + R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {region=0}")", + R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {region=0}")", + R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {region=0}")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {region=0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index f58eee73..fb4bd233 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -68,3 +68,34 @@ unsigned numPlayingVoices(const sfz::Synth& synth) return !v->releasedOrFree(); }); } + +std::string createReferenceGraph(std::vector lines, int numRegions) +{ + for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { + lines.push_back(absl::StrCat( + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {region=)", + regionIdx, + R"(}")" + )); + lines.push_back(absl::StrCat( + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {region=)", + regionIdx, + R"(}")" + )); + } + + std::sort(lines.begin(), lines.end()); + + std::string graph; + graph.reserve(1024); + + graph += "digraph {\n"; + for (const std::string& line : lines) { + graph.push_back('\t'); + graph += line; + graph.push_back('\n'); + } + graph += "}\n"; + + return graph; +}; diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index e5e48671..272fb612 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -65,3 +65,24 @@ const std::vector getPlayingVoices(const sfz::Synth& synth); * @return unsigned */ unsigned numPlayingVoices(const sfz::Synth& synth); + +/** + * @brief Create the dot graph representation from a list of strings + * + */ +std::string createReferenceGraph(std::vector lines, int numRegions = 1); + +template +inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) +{ + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0; i < rhs.size(); ++i) + if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { + std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; + return false; + } + + return true; +} From 169ba940c98f2e97f8373c5476b1d2f6652a69e1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 11:55:25 +0200 Subject: [PATCH 9/9] Print the modulation key more compactly --- src/sfizz/modulations/ModKey.cpp | 16 ++++++++-------- tests/FlexEGT.cpp | 14 +++++++------- tests/ModulationsT.cpp | 8 ++++---- tests/TestHelpers.cpp | 4 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 9012fb46..aa4dd1cd 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -71,22 +71,22 @@ std::string ModKey::toString() const " {curve=", params_.curve, ", smooth=", params_.smooth, ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: - return absl::StrCat("EG ", 1 + params_.N, " {region=", region_.number(), "}"); + return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: - return absl::StrCat("LFO ", 1 + params_.N, " {region=", region_.number(), "}"); + return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}"); case ModId::Amplitude: - return absl::StrCat("Amplitude", " {region=", region_.number(), "}"); + return absl::StrCat("Amplitude {", region_.number(), "}"); case ModId::Pan: - return absl::StrCat("Pan", " {region=", region_.number(), "}"); + return absl::StrCat("Pan {", region_.number(), "}"); case ModId::Width: - return absl::StrCat("Width", " {region=", region_.number(), "}"); + return absl::StrCat("Width {", region_.number(), "}"); case ModId::Position: - return absl::StrCat("Position", " {region=", region_.number(), "}"); + return absl::StrCat("Position {", region_.number(), "}"); case ModId::Pitch: - return absl::StrCat("Pitch", " {region=", region_.number(), "}"); + return absl::StrCat("Pitch {", region_.number(), "}"); case ModId::Volume: - return absl::StrCat("Volume", " {region=", region_.number(), "}"); + return absl::StrCat("Volume {", region_.number(), "}"); default: return {}; diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index f48e5455..db16f93c 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -41,7 +41,7 @@ TEST_CASE("[FlexEG] Values") REQUIRE( egDescription.points[4].level == 1.0_a ); REQUIRE( egDescription.sustain == 3 ); REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ - R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + R"("EG 1 {0}" -> "Amplitude {0}")", })); } @@ -85,12 +85,12 @@ TEST_CASE("[FlexEG] Connections") REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ - R"("EG 1 {region=0}" -> "Amplitude {region=0}")", - R"("EG 1 {region=1}" -> "Pan {region=1}")", - R"("EG 1 {region=2}" -> "Width {region=2}")", - R"("EG 1 {region=3}" -> "Position {region=3}")", - R"("EG 1 {region=4}" -> "Pitch {region=4}")", - R"("EG 1 {region=5}" -> "Volume {region=5}")", + R"("EG 1 {0}" -> "Amplitude {0}")", + R"("EG 1 {1}" -> "Pan {1}")", + R"("EG 1 {2}" -> "Width {2}")", + R"("EG 1 {3}" -> "Position {3}")", + R"("EG 1 {4}" -> "Pitch {4}")", + R"("EG 1 {5}" -> "Volume {5}")", }, 6)); } diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 627714d6..2b14e191 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -93,9 +93,9 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {region=0}")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {region=0}")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {region=0}")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {region=0}")", + 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}")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index fb4bd233..b3521c4a 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -73,12 +73,12 @@ std::string createReferenceGraph(std::vector lines, int numRegions) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { lines.push_back(absl::StrCat( - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {region=)", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {)", regionIdx, R"(}")" )); lines.push_back(absl::StrCat( - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {region=)", + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {)", regionIdx, R"(}")" ));