commit
f0dd146361
25 changed files with 1066 additions and 56 deletions
3
dpf.mk
3
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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<float>(curve._points));
|
||||
return curve;
|
||||
}
|
||||
|
||||
const Curve& Curve::getDefault()
|
||||
{
|
||||
return defaultCurve;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -244,6 +244,20 @@ namespace Default
|
|||
constexpr Range<float> egOnCCTimeRange { -100.0, 100.0 };
|
||||
constexpr Range<float> 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<int> flexEGDynamicRange { 0, 1 };
|
||||
constexpr Range<int> flexEGSustainRange { 0, 100 };
|
||||
constexpr Range<float> flexEGPointTimeRange { 0.0f, 100.0f };
|
||||
constexpr Range<float> flexEGPointLevelRange { -1.0f, 1.0f };
|
||||
constexpr Range<float> flexEGPointShapeRange { -100.0f, 100.0f };
|
||||
|
||||
// ***** SFZ v2 ********
|
||||
constexpr int sampleQuality { 1 };
|
||||
constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive
|
||||
|
|
|
|||
87
src/sfizz/FlexEGDescription.cpp
Normal file
87
src/sfizz/FlexEGDescription.cpp
Normal file
|
|
@ -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 <absl/container/flat_hash_map.h>
|
||||
#include <cmath>
|
||||
|
||||
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<float, std::weak_ptr<Curve>> FlexEGShapes;
|
||||
|
||||
static FlexEGShapes& getShapeMap()
|
||||
{
|
||||
static FlexEGShapes shapes;
|
||||
return shapes;
|
||||
}
|
||||
|
||||
std::shared_ptr<Curve> FlexEGs::getShapeCurve(float shape)
|
||||
{
|
||||
static FlexEGShapes& map = getShapeMap();
|
||||
|
||||
std::weak_ptr<Curve>& slot = map[shape];
|
||||
|
||||
std::shared_ptr<Curve> 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
|
||||
39
src/sfizz/FlexEGDescription.h
Normal file
39
src/sfizz/FlexEGDescription.h
Normal file
|
|
@ -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 <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace sfz {
|
||||
class Curve;
|
||||
|
||||
namespace FlexEGs {
|
||||
std::shared_ptr<Curve> 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<Curve> 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<FlexEGPoint> points;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
207
src/sfizz/FlexEnvelope.cpp
Normal file
207
src/sfizz/FlexEnvelope.cpp
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// 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
|
||||
- [ ] egN_loop
|
||||
- [ ] egN_loop_shape
|
||||
- [ ] egN_loop_count
|
||||
*/
|
||||
|
||||
#include "FlexEnvelope.h"
|
||||
#include "FlexEGDescription.h"
|
||||
#include "Curve.h"
|
||||
#include "Config.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include <absl/types/optional.h>
|
||||
|
||||
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<size_t> currentFramesUntilRelease_ { absl::nullopt };
|
||||
bool isReleased_ { false };
|
||||
|
||||
//
|
||||
void process(absl::Span<float> 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<float> out)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
impl.process(out);
|
||||
}
|
||||
|
||||
void FlexEnvelope::Impl::process(absl::Span<float> 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;
|
||||
|
||||
while (frameIndex < numFrames) {
|
||||
// Check for release
|
||||
if (currentFramesUntilRelease_ && *currentFramesUntilRelease_ == 0) {
|
||||
isReleased_ = true;
|
||||
currentFramesUntilRelease_ = absl::nullopt;
|
||||
}
|
||||
|
||||
// Perform stage transitions
|
||||
const bool isReleased = isReleased_;
|
||||
while ((!stageSustained_ && currentTime_ >= stageTime_) ||
|
||||
(stageSustained_ && isReleased)) {
|
||||
if (!advanceToNextStage()) {
|
||||
out.remove_prefix(frameIndex);
|
||||
fill(out, 0.0f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Process without going past the release point, if there is one
|
||||
size_t maxFrameIndex = numFrames;
|
||||
if (currentFramesUntilRelease_)
|
||||
maxFrameIndex = std::min(maxFrameIndex, frameIndex + *currentFramesUntilRelease_);
|
||||
|
||||
// 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) {
|
||||
time += samplePeriod;
|
||||
float x = time * (1.0f / stageEndTime);
|
||||
float c = curve.evalNormalized(x);
|
||||
level = sourceLevel + c * (targetLevel - sourceLevel);
|
||||
out[frameIndex++] = level;
|
||||
++framesDone;
|
||||
}
|
||||
currentLevel_ = level;
|
||||
|
||||
// Update the counter to release
|
||||
if (currentFramesUntilRelease_)
|
||||
*currentFramesUntilRelease_ -= framesDone;
|
||||
|
||||
currentTime_ = time;
|
||||
}
|
||||
}
|
||||
|
||||
bool FlexEnvelope::Impl::advanceToNextStage()
|
||||
{
|
||||
const FlexEGDescription& desc = *desc_;
|
||||
|
||||
unsigned nextStageNo = currentStageNumber_ + 1;
|
||||
currentStageNumber_ = nextStageNo;
|
||||
|
||||
if (nextStageNo >= desc.points.size())
|
||||
return false;
|
||||
|
||||
const FlexEGPoint& point = desc.points[nextStageNo];
|
||||
stageSourceLevel_ = currentLevel_;
|
||||
stageTargetLevel_ = point.level;
|
||||
stageTime_ = point.time;
|
||||
stageSustained_ = int(nextStageNo) == desc.sustain;
|
||||
stageCurve_ = &point.curve();
|
||||
|
||||
currentTime_ = 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
53
src/sfizz/FlexEnvelope.h
Normal file
53
src/sfizz/FlexEnvelope.h
Normal file
|
|
@ -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 <absl/types/span.h>
|
||||
#include <memory>
|
||||
|
||||
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<float> out);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<FlexEGDescription> flexEGs;
|
||||
|
||||
// LFOs
|
||||
std::vector<LFODescription> lfos;
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -484,6 +486,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();
|
||||
|
|
@ -599,6 +604,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;
|
||||
}
|
||||
|
|
@ -609,6 +615,7 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
settingsPerVoice.maxFilters = maxFilters;
|
||||
settingsPerVoice.maxEQs = maxEQs;
|
||||
settingsPerVoice.maxLFOs = maxLFOs;
|
||||
settingsPerVoice.maxFlexEGs = maxFlexEGs;
|
||||
|
||||
applySettingsPerVoice();
|
||||
|
||||
|
|
@ -1479,6 +1486,7 @@ void sfz::Synth::applySettingsPerVoice()
|
|||
voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters);
|
||||
voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs);
|
||||
voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs);
|
||||
voice->setMaxFlexEGsPerVoice(settingsPerVoice.maxFlexEGs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1497,6 +1505,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;
|
||||
|
|
|
|||
|
|
@ -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<ControllerSource> genController;
|
||||
std::unique_ptr<LFOSource> genLFO;
|
||||
std::unique_ptr<FlexEnvelopeSource> genFlexEnvelope;
|
||||
|
||||
// Settings per voice
|
||||
struct SettingsPerVoice {
|
||||
size_t maxFilters { 0 };
|
||||
size_t maxEQs { 0 };
|
||||
size_t maxLFOs { 0 };
|
||||
size_t maxFlexEGs { 0 };
|
||||
};
|
||||
SettingsPerVoice settingsPerVoice;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<FlexEnvelope>();
|
||||
eg->setSampleRate(sampleRate);
|
||||
flexEGs[i] = std::move(eg);
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::setupOscillatorUnison()
|
||||
{
|
||||
int m = region->oscillatorMulti;
|
||||
|
|
|
|||
|
|
@ -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<FilterHolderPtr> filters;
|
||||
std::vector<EQHolderPtr> equalizers;
|
||||
std::vector<std::unique_ptr<LFO>> lfos;
|
||||
std::vector<std::unique_ptr<FlexEnvelope>> flexEGs;
|
||||
|
||||
ADSREnvelope<float> egEnvelope;
|
||||
float bendStepFactor { centsFactor(1) };
|
||||
|
|
|
|||
|
|
@ -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_.number(), "}");
|
||||
case ModId::LFO:
|
||||
return absl::StrCat("LFO ", 1 + params_.N);
|
||||
return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}");
|
||||
|
||||
case ModId::Amplitude:
|
||||
return "Amplitude";
|
||||
return absl::StrCat("Amplitude {", region_.number(), "}");
|
||||
case ModId::Pan:
|
||||
return "Pan";
|
||||
return absl::StrCat("Pan {", region_.number(), "}");
|
||||
case ModId::Width:
|
||||
return "Width";
|
||||
return absl::StrCat("Width {", region_.number(), "}");
|
||||
case ModId::Position:
|
||||
return "Position";
|
||||
return absl::StrCat("Position {", region_.number(), "}");
|
||||
case ModId::Pitch:
|
||||
return "Pitch";
|
||||
return absl::StrCat("Pitch {", region_.number(), "}");
|
||||
case ModId::Volume:
|
||||
return "Volume";
|
||||
return absl::StrCat("Volume {", region_.number(), "}");
|
||||
|
||||
default:
|
||||
return {};
|
||||
|
|
|
|||
86
src/sfizz/modulations/sources/FlexEnvelope.cpp
Normal file
86
src/sfizz/modulations/sources/FlexEnvelope.cpp
Normal file
|
|
@ -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<Voice> 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<Voice> 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<Voice> voiceId, absl::Span<float> 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
|
||||
24
src/sfizz/modulations/sources/FlexEnvelope.h
Normal file
24
src/sfizz/modulations/sources/FlexEnvelope.h
Normal file
|
|
@ -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<Voice> voiceId, unsigned delay) override;
|
||||
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
||||
|
||||
private:
|
||||
Synth* synth_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
|
@ -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 <absl/algorithm/container.h>
|
||||
#include <absl/types/span.h>
|
||||
|
|
@ -13,21 +14,6 @@
|
|||
#include <iostream>
|
||||
using namespace Catch::literals;
|
||||
|
||||
template <class Type>
|
||||
inline bool approxEqual(absl::Span<const Type> lhs, absl::Span<const Type> 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<float> envelope;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "catch2/catch.hpp"
|
||||
#include <algorithm>
|
||||
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<float, sfz::Curve::NumValues> curvePoints;
|
||||
float val = 0.0f;
|
||||
float step = 1 / static_cast<float>(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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
263
tests/FlexEGT.cpp
Normal file
263
tests/FlexEGT.cpp
Normal file
|
|
@ -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"(
|
||||
<region> 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 {0}" -> "Amplitude {0}")",
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_CASE("[FlexEG] Default values")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
|
||||
synth.loadSfzString(fs::current_path(), R"(
|
||||
<region> 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"(
|
||||
<region> sample=*sine eg1_amplitude=1 eg1_time1=.1 eg1_level1=.25
|
||||
<region> sample=*sine eg1_pan=1 eg1_time1=.1 eg1_level1=.25
|
||||
<region> sample=*sine eg1_width=1 eg1_time1=.1 eg1_level1=.25
|
||||
<region> sample=*sine eg1_position=1 eg1_time1=.1 eg1_level1=.25
|
||||
<region> sample=*sine eg1_pitch=1 eg1_time1=.1 eg1_level1=.25
|
||||
<region> 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 {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));
|
||||
}
|
||||
|
||||
TEST_CASE("[FlexEG] Coarse numerical envelope test (No release)")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
|
||||
synth.loadSfzString(fs::current_path(), R"(
|
||||
<region> 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<float> 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"(
|
||||
<region> 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<float> output;
|
||||
std::vector<float> 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<float>(output, expected) );
|
||||
}
|
||||
|
||||
TEST_CASE("[FlexEG] Coarse numerical envelope test (with release)")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
|
||||
synth.loadSfzString(fs::current_path(), R"(
|
||||
<region> 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<float> 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"(
|
||||
<region> 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<float> output;
|
||||
std::vector<float> 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<float>(output, expected) );
|
||||
}
|
||||
|
||||
TEST_CASE("[FlexEG] Coarse numerical envelope test (with shapes)")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
|
||||
synth.loadSfzString(fs::current_path(), R"(
|
||||
<region> 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<float> 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"(
|
||||
<region> 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<float> output;
|
||||
std::vector<float> 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<float>(output, expected, 0.01f) );
|
||||
}
|
||||
|
|
@ -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<std::string> 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 {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}")",
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,3 +68,34 @@ unsigned numPlayingVoices(const sfz::Synth& synth)
|
|||
return !v->releasedOrFree();
|
||||
});
|
||||
}
|
||||
|
||||
std::string createReferenceGraph(std::vector<std::string> 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 {)",
|
||||
regionIdx,
|
||||
R"(}")"
|
||||
));
|
||||
lines.push_back(absl::StrCat(
|
||||
R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {)",
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,3 +65,24 @@ const std::vector<const sfz::Voice*> 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<std::string> lines, int numRegions = 1);
|
||||
|
||||
template <class Type>
|
||||
inline bool approxEqual(absl::Span<const Type> lhs, absl::Span<const Type> 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue