commit
a12cf4e24c
30 changed files with 1661 additions and 403 deletions
5
dpf.mk
5
dpf.mk
|
|
@ -60,6 +60,11 @@ SFIZZ_SOURCES = \
|
|||
src/sfizz/Curve.cpp \
|
||||
src/sfizz/effects/Apan.cpp \
|
||||
src/sfizz/Effects.cpp \
|
||||
src/sfizz/modulations/ModId.cpp \
|
||||
src/sfizz/modulations/ModKey.cpp \
|
||||
src/sfizz/modulations/ModKeyHash.cpp \
|
||||
src/sfizz/modulations/ModMatrix.cpp \
|
||||
src/sfizz/modulations/sources/Controller.cpp \
|
||||
src/sfizz/effects/Compressor.cpp \
|
||||
src/sfizz/effects/Disto.cpp \
|
||||
src/sfizz/effects/Eq.cpp \
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ set (SFIZZ_HEADERS
|
|||
sfizz/Debug.h
|
||||
sfizz/utility/SpinMutex.h
|
||||
sfizz/utility/SpinMutex.cpp
|
||||
sfizz/modulations/ModId.h
|
||||
sfizz/modulations/ModKey.h
|
||||
sfizz/modulations/ModKeyHash.h
|
||||
sfizz/modulations/ModMatrix.h
|
||||
sfizz/modulations/ModGenerator.h
|
||||
sfizz/modulations/sources/Controller.h
|
||||
sfizz/effects/impl/ResonantArray.h
|
||||
sfizz/effects/impl/ResonantArrayAVX.h
|
||||
sfizz/effects/impl/ResonantArraySSE.h
|
||||
|
|
@ -67,7 +73,6 @@ set (SFIZZ_HEADERS
|
|||
sfizz/MathHelpers.h
|
||||
sfizz/MidiState.h
|
||||
sfizz/ModifierHelpers.h
|
||||
sfizz/Modifiers.h
|
||||
sfizz/NumericId.h
|
||||
sfizz/OnePoleFilter.h
|
||||
sfizz/Oversampler.h
|
||||
|
|
@ -128,6 +133,11 @@ set (SFIZZ_SOURCES
|
|||
sfizz/RTSemaphore.cpp
|
||||
sfizz/Panning.cpp
|
||||
sfizz/Effects.cpp
|
||||
sfizz/modulations/ModId.cpp
|
||||
sfizz/modulations/ModKey.cpp
|
||||
sfizz/modulations/ModKeyHash.cpp
|
||||
sfizz/modulations/ModMatrix.cpp
|
||||
sfizz/modulations/sources/Controller.cpp
|
||||
sfizz/effects/Nothing.cpp
|
||||
sfizz/effects/Filter.cpp
|
||||
sfizz/effects/Eq.cpp
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
#include "Range.h"
|
||||
#include "Defaults.h"
|
||||
#include "Modifiers.h"
|
||||
#include "Resources.h"
|
||||
#include "SfzHelpers.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace sfz {
|
||||
|
|
@ -257,77 +256,4 @@ void pitchBendEnvelope(const EventVector& events, absl::Span<float> envelope, F&
|
|||
multiplicativeEnvelope<F>(events, envelope, std::forward<F>(lambda));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds a linear envelope, possibly quantized, based on the events fetched
|
||||
* from a midi state and the modifier data. This is a helper function for recurrent
|
||||
* code in the voice logic.
|
||||
*
|
||||
* @tparam F
|
||||
* @param resources
|
||||
* @param span
|
||||
* @param ccData
|
||||
* @param lambda
|
||||
*/
|
||||
template <class F>
|
||||
void linearModifier(const sfz::Resources& resources, absl::Span<float> span, const sfz::CCData<sfz::Modifier>& ccData, F&& lambda)
|
||||
{
|
||||
const auto& events = resources.midiState.getCCEvents(ccData.cc);
|
||||
const auto& curve = resources.curves.getCurve(ccData.data.curve);
|
||||
if (ccData.data.step == 0.0f) {
|
||||
linearEnvelope(events, span, [&ccData, &curve, &lambda](float x) {
|
||||
return lambda(curve.evalNormalized(x) * ccData.data.value);
|
||||
});
|
||||
} else {
|
||||
const float stepSize { lambda(ccData.data.step) };
|
||||
linearEnvelope(
|
||||
events, span, [&ccData, &curve, &lambda](float x) {
|
||||
return lambda(curve.evalNormalized(x) * ccData.data.value);
|
||||
},
|
||||
stepSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds a multiplicative envelope, possibly quantized, based on the events fetched
|
||||
* from a midi state and the modifier data. This is a helper function for recurrent
|
||||
* code in the voice logic.
|
||||
*
|
||||
* @tparam F
|
||||
* @param resources
|
||||
* @param span
|
||||
* @param ccData
|
||||
* @param lambda
|
||||
*/
|
||||
template <class F>
|
||||
void multiplicativeModifier(const sfz::Resources& resources, absl::Span<float> span, const sfz::CCData<sfz::Modifier>& ccData, F&& lambda)
|
||||
{
|
||||
const auto& events = resources.midiState.getCCEvents(ccData.cc);
|
||||
const auto& curve = resources.curves.getCurve(ccData.data.curve);
|
||||
if (ccData.data.step == 0.0f) {
|
||||
multiplicativeEnvelope(events, span, [&ccData, &curve, &lambda](float x) {
|
||||
return lambda(curve.evalNormalized(x) * ccData.data.value);
|
||||
});
|
||||
} else {
|
||||
const float stepSize { lambda(ccData.data.step) };
|
||||
multiplicativeEnvelope(
|
||||
events, span, [&ccData, &curve, &lambda](float x) {
|
||||
return lambda(curve.evalNormalized(x) * ccData.data.value);
|
||||
},
|
||||
stepSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Alias for a simple linear modifier with no lambda
|
||||
*
|
||||
* @tparam F
|
||||
* @param resources
|
||||
* @param span
|
||||
* @param ccData
|
||||
* @param lambda
|
||||
*/
|
||||
inline void linearModifier(const sfz::Resources& resources, absl::Span<float> span, const sfz::CCData<sfz::Modifier>& ccData)
|
||||
{
|
||||
linearModifier(resources, span, ccData, [](float x) { return x; });
|
||||
}
|
||||
}
|
||||
} // namespace sfz
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
// 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 "Config.h"
|
||||
#include <numeric>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
/**
|
||||
* @brief Base modifier class
|
||||
*
|
||||
*/
|
||||
struct Modifier {
|
||||
float value { 0.0f };
|
||||
float step { 0.0f };
|
||||
uint8_t curve { 0 };
|
||||
uint8_t smooth { 0 };
|
||||
static_assert(config::maxCurves - 1 <= std::numeric_limits<decltype(curve)>::max(), "The curve type in the Modifier struct cannot support the required number of curves");
|
||||
};
|
||||
|
||||
enum class Mod : size_t {
|
||||
amplitude = 0,
|
||||
pan,
|
||||
width,
|
||||
position,
|
||||
pitch,
|
||||
volume,
|
||||
sentinel
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Vectors of elements indexed on modifiers with casting and iterators
|
||||
*
|
||||
* @tparam T
|
||||
*/
|
||||
template <class T>
|
||||
class ModifierVector : public std::vector<T> {
|
||||
public:
|
||||
T& operator[](sfz::Mod idx) { return this->std::vector<T>::operator[](static_cast<size_t>(idx)); }
|
||||
const T& operator[](sfz::Mod idx) const { return this->std::vector<T>::operator[](static_cast<size_t>(idx)); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Array of elements indexed on modifiers with casting and iterators
|
||||
*
|
||||
* @tparam T
|
||||
*/
|
||||
template <class T>
|
||||
class ModifierArray {
|
||||
public:
|
||||
using ContainerType = typename std::array<T, (size_t)Mod::sentinel>;
|
||||
using iterator = typename ContainerType::iterator;
|
||||
using const_iterator = typename ContainerType::const_iterator;
|
||||
ModifierArray() = default;
|
||||
ModifierArray(T val)
|
||||
{
|
||||
std::fill(underlying.begin(), underlying.end(), val);
|
||||
}
|
||||
ModifierArray(std::array<T, (size_t)Mod::sentinel>&& array) : underlying(array) {}
|
||||
T& operator[](sfz::Mod idx) { return underlying.operator[](static_cast<size_t>(idx)); }
|
||||
const T& operator[](sfz::Mod idx) const { return underlying.operator[](static_cast<size_t>(idx)); }
|
||||
iterator begin() { return underlying.begin(); }
|
||||
iterator end() { return underlying.end(); }
|
||||
const_iterator begin() const { return underlying.begin(); }
|
||||
const_iterator end() const { return underlying.end(); }
|
||||
private:
|
||||
ContainerType underlying {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper for iterating over all possible modifiers.
|
||||
* Should fail at compile time if you update the modifiers but not this.
|
||||
*
|
||||
*/
|
||||
static const ModifierArray<Mod> allModifiers {{
|
||||
Mod::amplitude,
|
||||
Mod::pan,
|
||||
Mod::width,
|
||||
Mod::position,
|
||||
Mod::pitch,
|
||||
Mod::volume
|
||||
}};
|
||||
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
#include "absl/types/optional.h"
|
||||
#include "absl/meta/type_traits.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include <string_view>
|
||||
#include "absl/strings/string_view.h"
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
#include <iosfwd>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "Opcode.h"
|
||||
#include "StringViewHelpers.h"
|
||||
#include "ModifierHelpers.h"
|
||||
#include "modulations/ModId.h"
|
||||
#include "absl/strings/str_replace.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/algorithm/container.h"
|
||||
|
|
@ -378,35 +379,35 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
|||
setValueFromOpcode(opcode, volume, Default::volumeRange);
|
||||
break;
|
||||
case_any_ccN("volume"): // also gain
|
||||
processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume]);
|
||||
processGenericCc(opcode, Default::volumeCCRange, ModKey::createNXYZ(ModId::Volume, id));
|
||||
break;
|
||||
case hash("amplitude"):
|
||||
if (auto value = readOpcode(opcode.value, Default::amplitudeRange))
|
||||
amplitude = normalizePercents(*value);
|
||||
break;
|
||||
case_any_ccN("amplitude"):
|
||||
processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude]);
|
||||
processGenericCc(opcode, Default::amplitudeRange, ModKey::createNXYZ(ModId::Amplitude, id));
|
||||
break;
|
||||
case hash("pan"):
|
||||
if (auto value = readOpcode(opcode.value, Default::panRange))
|
||||
pan = normalizePercents(*value);
|
||||
break;
|
||||
case_any_ccN("pan"):
|
||||
processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan]);
|
||||
processGenericCc(opcode, Default::panCCRange, ModKey::createNXYZ(ModId::Pan, id));
|
||||
break;
|
||||
case hash("position"):
|
||||
if (auto value = readOpcode(opcode.value, Default::positionRange))
|
||||
position = normalizePercents(*value);
|
||||
break;
|
||||
case_any_ccN("position"):
|
||||
processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position]);
|
||||
processGenericCc(opcode, Default::positionCCRange, ModKey::createNXYZ(ModId::Position, id));
|
||||
break;
|
||||
case hash("width"):
|
||||
if (auto value = readOpcode(opcode.value, Default::widthRange))
|
||||
width = normalizePercents(*value);
|
||||
break;
|
||||
case_any_ccN("width"):
|
||||
processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width]);
|
||||
processGenericCc(opcode, Default::widthCCRange, ModKey::createNXYZ(ModId::Width, id));
|
||||
break;
|
||||
case hash("amp_keycenter"):
|
||||
setValueFromOpcode(opcode, ampKeycenter, Default::keyRange);
|
||||
|
|
@ -770,7 +771,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
|||
setValueFromOpcode(opcode, tune, Default::tuneRange);
|
||||
break;
|
||||
case_any_ccN("pitch"): // also tune
|
||||
processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch]);
|
||||
processGenericCc(opcode, Default::tuneCCRange, ModKey::createNXYZ(ModId::Pitch, id));
|
||||
break;
|
||||
case hash("bend_up"): // also bendup
|
||||
setValueFromOpcode(opcode, bendUp, Default::bendBoundRange);
|
||||
|
|
@ -924,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool sfz::Region::processGenericCc(const Opcode& opcode, Range<float> range, CCMap<Modifier> *ccMap)
|
||||
bool sfz::Region::processGenericCc(const Opcode& opcode, Range<float> range, const ModKey& target)
|
||||
{
|
||||
if (!opcode.isAnyCcN())
|
||||
return false;
|
||||
|
|
@ -933,29 +934,51 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range<float> range, CCM
|
|||
if (ccNumber >= config::numCCs)
|
||||
return false;
|
||||
|
||||
if (ccMap) {
|
||||
Modifier& modifier = (*ccMap)[ccNumber];
|
||||
if (target) {
|
||||
// search an existing connection of same CC number and target
|
||||
// if it exists, modify, otherwise create
|
||||
auto it = std::find_if(connections.begin(), connections.end(),
|
||||
[ccNumber, &target](const Connection& x) -> bool
|
||||
{
|
||||
return x.source.id() == ModId::Controller &&
|
||||
x.source.parameters().cc == ccNumber &&
|
||||
x.target == target;
|
||||
});
|
||||
|
||||
Connection *conn;
|
||||
if (it != connections.end())
|
||||
conn = &*it;
|
||||
else {
|
||||
connections.emplace_back();
|
||||
conn = &connections.back();
|
||||
conn->source = ModKey::createCC(ccNumber, 0, 0, 0, 0);
|
||||
conn->target = target;
|
||||
}
|
||||
|
||||
//
|
||||
ModKey::Parameters p = conn->source.parameters();
|
||||
switch (opcode.category) {
|
||||
case kOpcodeOnCcN:
|
||||
setValueFromOpcode(opcode, modifier.value, range);
|
||||
setValueFromOpcode(opcode, p.value, range);
|
||||
break;
|
||||
case kOpcodeCurveCcN:
|
||||
setValueFromOpcode(opcode, modifier.curve, Default::curveCCRange);
|
||||
setValueFromOpcode(opcode, p.curve, Default::curveCCRange);
|
||||
break;
|
||||
case kOpcodeStepCcN:
|
||||
{
|
||||
const Range<float> stepCCRange { 0.0f, std::max(std::abs(range.getStart()), std::abs(range.getEnd())) };
|
||||
setValueFromOpcode(opcode, modifier.step, stepCCRange);
|
||||
setValueFromOpcode(opcode, p.step, stepCCRange);
|
||||
}
|
||||
break;
|
||||
case kOpcodeSmoothCcN:
|
||||
setValueFromOpcode(opcode, modifier.smooth, Default::smoothCCRange);
|
||||
setValueFromOpcode(opcode, p.smooth, Default::smoothCCRange);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
conn->source = ModKey(ModId::Controller, {}, p);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@
|
|||
#include "MidiState.h"
|
||||
#include "FileId.h"
|
||||
#include "NumericId.h"
|
||||
#include "Modifiers.h"
|
||||
#include "modulations/ModKey.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include <bitset>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -240,11 +241,11 @@ struct Region {
|
|||
*
|
||||
* @param opcode
|
||||
* @param range
|
||||
* @param ccMap
|
||||
* @param target
|
||||
* @return true if the opcode was properly read and stored.
|
||||
* @return false
|
||||
*/
|
||||
bool processGenericCc(const Opcode& opcode, Range<float> range, CCMap<Modifier> *ccMap);
|
||||
bool processGenericCc(const Opcode& opcode, Range<float> range, const ModKey& target);
|
||||
|
||||
void offsetAllKeys(int offset) noexcept;
|
||||
|
||||
|
|
@ -368,12 +369,17 @@ struct Region {
|
|||
// Effects
|
||||
std::vector<float> gainToEffect;
|
||||
|
||||
// Modifiers
|
||||
ModifierArray<CCMap<Modifier>> modifiers;
|
||||
|
||||
bool triggerOnCC { false }; // whether the region triggers on CC events or note events
|
||||
bool triggerOnNote { true };
|
||||
|
||||
// Modulation matrix connections
|
||||
struct Connection {
|
||||
ModKey source;
|
||||
ModKey target;
|
||||
float sourceDepth = 1.0f;
|
||||
};
|
||||
std::vector<Connection> connections;
|
||||
|
||||
// Parent
|
||||
RegionSet* parent { nullptr };
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "Wavetables.h"
|
||||
#include "Curve.h"
|
||||
#include "Tuning.h"
|
||||
#include "modulations/ModMatrix.h"
|
||||
#include "absl/types/optional.h"
|
||||
|
||||
namespace sfz
|
||||
|
|
@ -33,18 +34,21 @@ struct Resources
|
|||
WavetablePool wavePool;
|
||||
Tuning tuning;
|
||||
absl::optional<StretchTuning> stretch;
|
||||
ModMatrix modMatrix;
|
||||
|
||||
void setSampleRate(float samplerate)
|
||||
{
|
||||
midiState.setSampleRate(samplerate);
|
||||
filterPool.setSampleRate(samplerate);
|
||||
eqPool.setSampleRate(samplerate);
|
||||
modMatrix.setSampleRate(samplerate);
|
||||
}
|
||||
|
||||
void setSamplesPerBlock(int samplesPerBlock)
|
||||
{
|
||||
bufferPool.setBufferSize(samplesPerBlock);
|
||||
midiState.setSamplesPerBlock(samplesPerBlock);
|
||||
modMatrix.setSamplesPerBlock(samplesPerBlock);
|
||||
}
|
||||
|
||||
void clear()
|
||||
|
|
@ -54,6 +58,7 @@ struct Resources
|
|||
wavePool.clearFileWaves();
|
||||
logger.clear();
|
||||
midiState.reset();
|
||||
modMatrix.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@
|
|||
#include "ModifierHelpers.h"
|
||||
#include "ScopedFTZ.h"
|
||||
#include "StringViewHelpers.h"
|
||||
#include "modulations/ModMatrix.h"
|
||||
#include "modulations/ModKey.h"
|
||||
#include "modulations/ModId.h"
|
||||
#include "modulations/sources/Controller.h"
|
||||
#include "pugixml.hpp"
|
||||
#include "absl/algorithm/container.h"
|
||||
#include "absl/memory/memory.h"
|
||||
|
|
@ -35,6 +39,9 @@ sfz::Synth::Synth(int numVoices)
|
|||
effectFactory.registerStandardEffectTypes();
|
||||
effectBuses.reserve(5); // sufficient room for main and fx1-4
|
||||
resetVoices(numVoices);
|
||||
|
||||
// modulation sources
|
||||
genController.reset(new ControllerSource(resources));
|
||||
}
|
||||
|
||||
sfz::Synth::~Synth()
|
||||
|
|
@ -445,7 +452,6 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
|
||||
size_t maxFilters { 0 };
|
||||
size_t maxEQs { 0 };
|
||||
ModifierArray<size_t> maxModifiers { 0 };
|
||||
|
||||
while (currentRegionIndex < currentRegionCount) {
|
||||
auto region = regions[currentRegionIndex].get();
|
||||
|
|
@ -556,8 +562,6 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
region->registerTempo(2.0f);
|
||||
maxFilters = max(maxFilters, region->filters.size());
|
||||
maxEQs = max(maxEQs, region->equalizers.size());
|
||||
for (const auto& mod : allModifiers)
|
||||
maxModifiers[mod] = max(maxModifiers[mod], region->modifiers[mod].size());
|
||||
|
||||
++currentRegionIndex;
|
||||
}
|
||||
|
|
@ -567,9 +571,10 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
|
||||
settingsPerVoice.maxFilters = maxFilters;
|
||||
settingsPerVoice.maxEQs = maxEQs;
|
||||
settingsPerVoice.maxModifiers = maxModifiers;
|
||||
|
||||
applySettingsPerVoice();
|
||||
|
||||
setupModMatrix();
|
||||
}
|
||||
|
||||
bool sfz::Synth::loadScalaFile(const fs::path& path)
|
||||
|
|
@ -717,6 +722,9 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
return;
|
||||
}
|
||||
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
mm.beginCycle(numFrames);
|
||||
|
||||
activeVoices = 0;
|
||||
{ // Main render block
|
||||
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
|
||||
|
|
@ -736,6 +744,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
if (voice->isFree())
|
||||
continue;
|
||||
|
||||
mm.beginVoice(voice->getId(), voice->getRegion()->getId());
|
||||
|
||||
activeVoices++;
|
||||
renderVoiceToOutputs(*voice, *tempSpan);
|
||||
callbackBreakdown.data += voice->getLastDataDuration();
|
||||
|
|
@ -743,6 +753,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
callbackBreakdown.filters += voice->getLastFilterDuration();
|
||||
callbackBreakdown.panning += voice->getLastPanningDuration();
|
||||
|
||||
mm.endVoice();
|
||||
|
||||
if (voice->toBeCleanedUp())
|
||||
voice->reset();
|
||||
}
|
||||
|
|
@ -770,6 +782,9 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
// Apply the master volume
|
||||
buffer.applyGain(db2mag(volume));
|
||||
|
||||
// Perform any remaining modulators
|
||||
mm.endCycle();
|
||||
|
||||
{ // Clear events and advance midi time
|
||||
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
||||
resources.midiState.advanceTime(buffer.getNumFrames());
|
||||
|
|
@ -1331,10 +1346,55 @@ void sfz::Synth::applySettingsPerVoice()
|
|||
for (auto& voice : voices) {
|
||||
voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters);
|
||||
voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs);
|
||||
voice->prepareSmoothers(settingsPerVoice.maxModifiers);
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Synth::setupModMatrix()
|
||||
{
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
|
||||
for (const RegionPtr& region : regions) {
|
||||
for (const Region::Connection& conn : region->connections) {
|
||||
ModGenerator* gen = nullptr;
|
||||
|
||||
switch (conn.source.id()) {
|
||||
case ModId::Controller:
|
||||
gen = genController.get();
|
||||
break;
|
||||
default:
|
||||
DBG("[sfizz] Have unknown type of source generator");
|
||||
break;
|
||||
}
|
||||
|
||||
ASSERT(gen);
|
||||
if (!gen)
|
||||
continue;
|
||||
|
||||
ModMatrix::SourceId source = mm.registerSource(conn.source, *gen);
|
||||
ModMatrix::TargetId target = mm.registerTarget(conn.target);
|
||||
|
||||
ASSERT(source);
|
||||
if (!source) {
|
||||
DBG("[sfizz] Failed to register modulation source");
|
||||
continue;
|
||||
}
|
||||
|
||||
ASSERT(target);
|
||||
if (!target) {
|
||||
DBG("[sfizz] Failed to register modulation target");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mm.connect(source, target, conn.sourceDepth)) {
|
||||
DBG("[sfizz] Failed to connect modulation source and target");
|
||||
ASSERTFALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mm.init();
|
||||
}
|
||||
|
||||
void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
||||
{
|
||||
const std::lock_guard<SpinMutex> disableCallback { callbackGuard };
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@
|
|||
#include "parser/Parser.h"
|
||||
#include "VoiceStealing.h"
|
||||
#include "utility/SpinMutex.h"
|
||||
#include "absl/types/span.h"
|
||||
#include <absl/types/span.h>
|
||||
#include <absl/types/optional.h>
|
||||
#include <absl/strings/string_view.h>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace sfz {
|
||||
class ControllerSource;
|
||||
|
||||
/**
|
||||
* @brief This class is the core of the sfizz library. In C++ it is the main point
|
||||
* of entry and in C the interface basically maps the functions of the class into
|
||||
|
|
@ -677,6 +679,11 @@ private:
|
|||
*/
|
||||
void applySettingsPerVoice();
|
||||
|
||||
/**
|
||||
* @brief Establish all connections of the modulation matrix.
|
||||
*/
|
||||
void setupModMatrix();
|
||||
|
||||
/**
|
||||
* @brief Render the voice to its designated outputs and effect busses.
|
||||
*
|
||||
|
|
@ -758,11 +765,13 @@ private:
|
|||
int noteOffset { 0 };
|
||||
int octaveOffset { 0 };
|
||||
|
||||
// Modulation source generators
|
||||
std::unique_ptr<ControllerSource> genController;
|
||||
|
||||
// Settings per voice
|
||||
struct SettingsPerVoice {
|
||||
size_t maxFilters { 0 };
|
||||
size_t maxEQs { 0 };
|
||||
ModifierArray<size_t> maxModifiers { 0 };
|
||||
};
|
||||
SettingsPerVoice settingsPerVoice;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
#include "SIMDHelpers.h"
|
||||
#include "Panning.h"
|
||||
#include "SfzHelpers.h"
|
||||
#include "modulations/ModId.h"
|
||||
#include "modulations/ModKey.h"
|
||||
#include "modulations/ModMatrix.h"
|
||||
#include "Interpolators.h"
|
||||
#include "absl/algorithm/container.h"
|
||||
|
||||
|
|
@ -138,32 +141,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
|||
bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend())));
|
||||
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate);
|
||||
|
||||
for (auto& modId : allModifiers) {
|
||||
ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size());
|
||||
forEachWithSmoother(modId, [modId, this](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
const auto ccValue = resources.midiState.getCCValue(mod.cc);
|
||||
const auto& curve = resources.curves.getCurve(mod.data.curve);
|
||||
const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value;
|
||||
switch (modId) {
|
||||
case Mod::volume:
|
||||
smoother.reset(db2mag(finalValue));
|
||||
break;
|
||||
case Mod::pitch:
|
||||
smoother.reset(centsFactor(finalValue));
|
||||
break;
|
||||
case Mod::amplitude:
|
||||
case Mod::pan:
|
||||
case Mod::width:
|
||||
case Mod::position:
|
||||
smoother.reset(normalizePercents(finalValue));
|
||||
break;
|
||||
default:
|
||||
smoother.reset(finalValue);
|
||||
break;
|
||||
}
|
||||
smoother.setSmoothing(mod.data.smooth, sampleRate);
|
||||
});
|
||||
}
|
||||
resources.modMatrix.initVoice(id, region->getId());
|
||||
}
|
||||
|
||||
int sfz::Voice::getCurrentSampleQuality() const noexcept
|
||||
|
|
@ -374,30 +352,24 @@ void sfz::Voice::amplitudeEnvelope(absl::Span<float> modulationSpan) noexcept
|
|||
{
|
||||
const auto numSamples = modulationSpan.size();
|
||||
|
||||
auto tempSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!tempSpan)
|
||||
return;
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
const ModKey volumeKey = ModKey::createNXYZ(ModId::Volume, region->getId());
|
||||
const ModKey amplitudeKey = ModKey::createNXYZ(ModId::Amplitude, region->getId());
|
||||
|
||||
// AmpEG envelope
|
||||
egEnvelope.getBlock(modulationSpan);
|
||||
|
||||
// Amplitude envelope
|
||||
applyGain1<float>(baseGain, modulationSpan);
|
||||
forEachWithSmoother(Mod::amplitude, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
linearModifier(resources, *tempSpan, mod, normalizePercents<float>);
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
applyGain<float>(*tempSpan, modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(amplitudeKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
modulationSpan[i] *= normalizePercents(mod[i]);
|
||||
}
|
||||
|
||||
// Volume envelope
|
||||
applyGain1<float>(db2mag(baseVolumedB), modulationSpan);
|
||||
forEachWithSmoother(Mod::volume, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
multiplicativeModifier(resources, *tempSpan, mod, [](float x) {
|
||||
return db2mag(x);
|
||||
});
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
applyGain<float>(*tempSpan, modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(volumeKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
modulationSpan[i] *= db2mag(mod[i]);
|
||||
}
|
||||
|
||||
// Smooth the gain transitions
|
||||
gainSmoother.process(modulationSpan, modulationSpan);
|
||||
|
|
@ -442,20 +414,21 @@ void sfz::Voice::panStageMono(AudioSpan<float> buffer) noexcept
|
|||
const auto rightBuffer = buffer.getSpan(1);
|
||||
|
||||
auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
auto tempSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!modulationSpan || !tempSpan)
|
||||
if (!modulationSpan)
|
||||
return;
|
||||
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId());
|
||||
|
||||
// Prepare for stereo output
|
||||
copy<float>(leftBuffer, rightBuffer);
|
||||
|
||||
// Apply panning
|
||||
fill(*modulationSpan, region->pan);
|
||||
forEachWithSmoother(Mod::pan, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
linearModifier(resources, *tempSpan, mod, normalizePercents<float>);
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
add<float>(*tempSpan, *modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(panKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
(*modulationSpan)[i] += normalizePercents(mod[i]);
|
||||
}
|
||||
pan(*modulationSpan, leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
|
|
@ -467,34 +440,35 @@ void sfz::Voice::panStageStereo(AudioSpan<float> buffer) noexcept
|
|||
const auto rightBuffer = buffer.getSpan(1);
|
||||
|
||||
auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
auto tempSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!modulationSpan || !tempSpan)
|
||||
if (!modulationSpan)
|
||||
return;
|
||||
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId());
|
||||
const ModKey widthKey = ModKey::createNXYZ(ModId::Width, region->getId());
|
||||
const ModKey positionKey = ModKey::createNXYZ(ModId::Position, region->getId());
|
||||
|
||||
// Apply panning
|
||||
fill(*modulationSpan, region->pan);
|
||||
forEachWithSmoother(Mod::pan, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
linearModifier(resources, *tempSpan, mod, normalizePercents<float>);
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
add<float>(*tempSpan, *modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(panKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
(*modulationSpan)[i] += normalizePercents(mod[i]);
|
||||
}
|
||||
pan(*modulationSpan, leftBuffer, rightBuffer);
|
||||
|
||||
// Apply the width/position process
|
||||
fill(*modulationSpan, region->width);
|
||||
forEachWithSmoother(Mod::width, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
linearModifier(resources, *tempSpan, mod, normalizePercents<float>);
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
add<float>(*tempSpan, *modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(widthKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
(*modulationSpan)[i] += normalizePercents(mod[i]);
|
||||
}
|
||||
width(*modulationSpan, leftBuffer, rightBuffer);
|
||||
|
||||
fill(*modulationSpan, region->position);
|
||||
forEachWithSmoother(Mod::position, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
linearModifier(resources, *tempSpan, mod, normalizePercents<float>);
|
||||
smoother.process(*tempSpan, *tempSpan);
|
||||
add<float>(*tempSpan, *modulationSpan);
|
||||
});
|
||||
if (float* mod = mm.getModulationByKey(positionKey)) {
|
||||
for (size_t i = 0; i < numSamples; ++i)
|
||||
(*modulationSpan)[i] += normalizePercents(mod[i]);
|
||||
}
|
||||
pan(*modulationSpan, leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
|
|
@ -881,12 +855,6 @@ void sfz::Voice::switchState(State s)
|
|||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::prepareSmoothers(const ModifierArray<size_t>& numModifiers)
|
||||
{
|
||||
for (auto& mod : allModifiers)
|
||||
modifierSmoothers[mod].resize(numModifiers[mod]);
|
||||
}
|
||||
|
||||
void sfz::Voice::pitchEnvelope(absl::Span<float> pitchSpan) noexcept
|
||||
{
|
||||
const auto numFrames = pitchSpan.size();
|
||||
|
|
@ -906,32 +874,17 @@ void sfz::Voice::pitchEnvelope(absl::Span<float> pitchSpan) noexcept
|
|||
bendSmoother.process(*bends, *bends);
|
||||
applyGain<float>(*bends, pitchSpan);
|
||||
|
||||
forEachWithSmoother(Mod::pitch, [&](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||
multiplicativeModifier(resources, *bends, mod, [](float x) {
|
||||
return centsFactor(x);
|
||||
});
|
||||
smoother.process(*bends, *bends);
|
||||
applyGain<float>(*bends, pitchSpan);
|
||||
});
|
||||
ModMatrix& mm = resources.modMatrix;
|
||||
const ModKey pitchKey = ModKey::createNXYZ(ModId::Pitch, region->getId());
|
||||
|
||||
if (float* mod = mm.getModulationByKey(pitchKey)) {
|
||||
for (size_t i = 0; i < numFrames; ++i)
|
||||
pitchSpan[i] *= centsFactor(mod[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::resetSmoothers() noexcept
|
||||
{
|
||||
for (auto& mod : allModifiers) {
|
||||
const auto resetValue = [mod] {
|
||||
switch (mod) {
|
||||
case Mod::volume: // fallthrough
|
||||
case Mod::pitch:
|
||||
return 1.0f;
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}();
|
||||
|
||||
for (auto& smoother : modifierSmoothers[mod]) {
|
||||
smoother.reset(resetValue);
|
||||
}
|
||||
}
|
||||
bendSmoother.reset(1.0f);
|
||||
gainSmoother.reset(0.0f);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,8 +302,6 @@ public:
|
|||
Duration getLastFilterDuration() const noexcept { return filterDuration; }
|
||||
Duration getLastPanningDuration() const noexcept { return panningDuration; }
|
||||
|
||||
void prepareSmoothers(const ModifierArray<size_t>& numModifiers);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Fill a span with data from a file source. This is the first step
|
||||
|
|
@ -390,27 +388,6 @@ private:
|
|||
*/
|
||||
void removeVoiceFromRing() noexcept;
|
||||
|
||||
/**
|
||||
* @brief Helper function to iterate jointly on modifiers and smoothers
|
||||
* for a given modulation target of type sfz::Mod
|
||||
*
|
||||
* @tparam F
|
||||
* @param modId
|
||||
* @param lambda
|
||||
*/
|
||||
template <class F>
|
||||
void forEachWithSmoother(sfz::Mod modId, F&& lambda)
|
||||
{
|
||||
size_t count = region->modifiers[modId].size();
|
||||
ASSERT(modifierSmoothers[modId].size() >= count);
|
||||
auto mod = region->modifiers[modId].begin();
|
||||
auto smoother = modifierSmoothers[modId].begin();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
lambda(*mod, *smoother);
|
||||
incrementAll(mod, smoother);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize frequency and gain coefficients for the oscillators.
|
||||
*/
|
||||
|
|
@ -479,7 +456,6 @@ private:
|
|||
fast_real_distribution<float> uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds };
|
||||
fast_gaussian_generator<float> gaussianNoiseDist { 0.0f, config::noiseVariance };
|
||||
|
||||
ModifierArray<std::vector<Smoother>> modifierSmoothers;
|
||||
Smoother gainSmoother;
|
||||
Smoother bendSmoother;
|
||||
Smoother xfadeSmoother;
|
||||
|
|
|
|||
66
src/sfizz/modulations/ModGenerator.h
Normal file
66
src/sfizz/modulations/ModGenerator.h
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// 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 "../NumericId.h"
|
||||
#include <absl/types/span.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
class ModKey;
|
||||
class Voice;
|
||||
|
||||
/**
|
||||
* @brief Generator for modulation sources
|
||||
*/
|
||||
class ModGenerator {
|
||||
public:
|
||||
virtual ~ModGenerator() {}
|
||||
|
||||
/**
|
||||
* @brief Set the sample rate
|
||||
*/
|
||||
virtual void setSampleRate(double sampleRate) { (void)sampleRate; }
|
||||
|
||||
/**
|
||||
* @brief Set the maximum block size
|
||||
*/
|
||||
virtual void setSamplesPerBlock(unsigned count) { (void)count; }
|
||||
|
||||
/**
|
||||
* @brief Initialize the generator.
|
||||
*
|
||||
* @param sourceKey identifier of the source to initialize
|
||||
* @param voiceId the particular voice to initialize, if per-voice
|
||||
*/
|
||||
virtual void init(const ModKey& sourceKey, NumericId<Voice> voiceId) = 0;
|
||||
|
||||
/**
|
||||
* @brief Generate a cycle of the modulator
|
||||
*
|
||||
* @param sourceKey source key
|
||||
* @param voiceNum voice number if the generator is per-voice, otherwise undefined
|
||||
* @param buffer output buffer
|
||||
*/
|
||||
virtual void generate(const ModKey& sourceKey, NumericId<Voice> voiceNum, absl::Span<float> buffer) = 0;
|
||||
|
||||
/**
|
||||
* @brief Advance the generator by a number of frames
|
||||
* This is called instead of `generate` in case the output is discarded.
|
||||
* It can be overriden with a faster implementation if wanted.
|
||||
*
|
||||
* @param sourceKey source key
|
||||
* @param voiceNum voice number if the generator is per-voice, otherwise undefined
|
||||
* @param buffer writable spare buffer, contents will be discarded
|
||||
*/
|
||||
virtual void generateDiscarded(const ModKey& sourceKey, NumericId<Voice> voiceNum, absl::Span<float> buffer)
|
||||
{
|
||||
generate(sourceKey, voiceNum, buffer);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
54
src/sfizz/modulations/ModId.cpp
Normal file
54
src/sfizz/modulations/ModId.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// 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 "ModId.h"
|
||||
|
||||
namespace sfz {
|
||||
|
||||
bool ModIds::isSource(ModId id) noexcept
|
||||
{
|
||||
return static_cast<int>(id) >= static_cast<int>(ModId::_SourcesStart) &&
|
||||
static_cast<int>(id) < static_cast<int>(ModId::_SourcesEnd);
|
||||
}
|
||||
|
||||
bool ModIds::isTarget(ModId id) noexcept
|
||||
{
|
||||
return static_cast<int>(id) >= static_cast<int>(ModId::_TargetsStart) &&
|
||||
static_cast<int>(id) < static_cast<int>(ModId::_TargetsEnd);
|
||||
}
|
||||
|
||||
int ModIds::flags(ModId id) noexcept
|
||||
{
|
||||
switch (id) {
|
||||
// sources
|
||||
case ModId::Controller:
|
||||
return kModIsPerCycle;
|
||||
case ModId::Envelope:
|
||||
return kModIsPerVoice;
|
||||
case ModId::LFO:
|
||||
return kModIsPerVoice;
|
||||
|
||||
// targets
|
||||
case ModId::Amplitude:
|
||||
return kModIsPerVoice|kModIsPercentMultiplicative;
|
||||
case ModId::Pan:
|
||||
return kModIsPerVoice|kModIsAdditive;
|
||||
case ModId::Width:
|
||||
return kModIsPerVoice|kModIsAdditive;
|
||||
case ModId::Position:
|
||||
return kModIsPerVoice|kModIsAdditive;
|
||||
case ModId::Pitch:
|
||||
return kModIsPerVoice|kModIsAdditive;
|
||||
case ModId::Volume:
|
||||
return kModIsPerVoice|kModIsAdditive;
|
||||
|
||||
// unknown
|
||||
default:
|
||||
return kModFlagsInvalid;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
87
src/sfizz/modulations/ModId.h
Normal file
87
src/sfizz/modulations/ModId.h
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
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace sfz {
|
||||
|
||||
/**
|
||||
* @brief Generic identifier of a kind of modulation source or target,
|
||||
* not necessarily unique per SFZ instrument
|
||||
*/
|
||||
enum class ModId : int {
|
||||
Undefined,
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Sources
|
||||
//--------------------------------------------------------------------------
|
||||
_SourcesStart,
|
||||
|
||||
Controller = _SourcesStart,
|
||||
Envelope,
|
||||
LFO,
|
||||
|
||||
_SourcesEnd,
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Targets
|
||||
//--------------------------------------------------------------------------
|
||||
_TargetsStart = _SourcesEnd,
|
||||
|
||||
Amplitude = _TargetsStart,
|
||||
Pan,
|
||||
Width,
|
||||
Position,
|
||||
Pitch,
|
||||
Volume,
|
||||
|
||||
_TargetsEnd,
|
||||
// [/targets] --------------------------------------------------------------
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Modulation bit flags (S=source, T=target, ST=either)
|
||||
*/
|
||||
enum ModFlags : int {
|
||||
//! This modulation is invalid. (ST)
|
||||
kModFlagsInvalid = -1,
|
||||
|
||||
//! This modulation is global (the default). (ST)
|
||||
kModIsPerCycle = 1 << 1,
|
||||
//! This modulation is updated separately for every region of every voice (ST)
|
||||
kModIsPerVoice = 1 << 2,
|
||||
|
||||
//! This target is additive. (T)
|
||||
kModIsAdditive = 1 << 3,
|
||||
//! This target is multiplicative (T)
|
||||
kModIsMultiplicative = 1 << 4,
|
||||
//! This target is %-multiplicative (T)
|
||||
kModIsPercentMultiplicative = 1 << 5,
|
||||
};
|
||||
|
||||
namespace ModIds {
|
||||
|
||||
bool isSource(ModId id) noexcept;
|
||||
bool isTarget(ModId id) noexcept;
|
||||
int flags(ModId id) noexcept;
|
||||
|
||||
template <class F> inline void forEachSourceId(F&& f)
|
||||
{
|
||||
for (int i = static_cast<int>(ModId::_SourcesStart);
|
||||
i < static_cast<int>(ModId::_SourcesEnd); ++i)
|
||||
f(static_cast<ModId>(i));
|
||||
}
|
||||
|
||||
template <class F> inline void forEachTargetId(F&& f)
|
||||
{
|
||||
for (int i = static_cast<int>(ModId::_TargetsStart);
|
||||
i < static_cast<int>(ModId::_TargetsEnd); ++i)
|
||||
f(static_cast<ModId>(i));
|
||||
}
|
||||
|
||||
} // namespace ModIds
|
||||
|
||||
} // namespace sfz
|
||||
123
src/sfizz/modulations/ModKey.cpp
Normal file
123
src/sfizz/modulations/ModKey.cpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// 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 "ModKey.h"
|
||||
#include "ModId.h"
|
||||
#include "../Debug.h"
|
||||
#include <absl/strings/str_cat.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
ModKey::Parameters::Parameters() noexcept
|
||||
{
|
||||
// zero-fill the structure
|
||||
// 1. this ensures that non-used values will be always 0
|
||||
// 2. this makes the object memcmp-comparable
|
||||
std::memset(this, 0, sizeof(*this));
|
||||
}
|
||||
|
||||
ModKey::Parameters::Parameters(const Parameters& other) noexcept
|
||||
{
|
||||
std::memcpy(this, &other, sizeof(*this));
|
||||
}
|
||||
|
||||
ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
std::memcpy(this, &other, sizeof(*this));
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool ModKey::Parameters::operator==(const Parameters& other) const noexcept
|
||||
{
|
||||
return std::memcmp(this, &other, sizeof(*this)) == 0;
|
||||
}
|
||||
|
||||
bool ModKey::Parameters::operator!=(const Parameters& other) const noexcept
|
||||
{
|
||||
return std::memcmp(this, &other, sizeof(*this)) != 0;
|
||||
}
|
||||
|
||||
ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step)
|
||||
{
|
||||
ModKey::Parameters p;
|
||||
p.cc = cc;
|
||||
p.curve = curve;
|
||||
p.smooth = smooth;
|
||||
p.value = value;
|
||||
p.step = step;
|
||||
return ModKey(ModId::Controller, {}, p);
|
||||
}
|
||||
|
||||
ModKey ModKey::createNXYZ(ModId id, NumericId<Region> region, uint8_t N, uint8_t X, uint8_t Y, uint8_t Z)
|
||||
{
|
||||
ASSERT(id != ModId::Controller);
|
||||
ModKey::Parameters p;
|
||||
p.N = N;
|
||||
p.X = X;
|
||||
p.Y = Y;
|
||||
p.Z = Z;
|
||||
return ModKey(id, region, p);
|
||||
}
|
||||
|
||||
bool ModKey::isSource() const noexcept
|
||||
{
|
||||
return ModIds::isSource(id_);
|
||||
}
|
||||
|
||||
bool ModKey::isTarget() const noexcept
|
||||
{
|
||||
return ModIds::isTarget(id_);
|
||||
}
|
||||
|
||||
int ModKey::flags() const noexcept
|
||||
{
|
||||
return ModIds::flags(id_);
|
||||
}
|
||||
|
||||
std::string ModKey::toString() const
|
||||
{
|
||||
switch (id_) {
|
||||
case ModId::Controller:
|
||||
return absl::StrCat("Controller ", params_.cc,
|
||||
" {curve=", params_.curve, ", smooth=", params_.smooth,
|
||||
", value=", params_.value, ", step=", params_.step, "}");
|
||||
case ModId::Envelope:
|
||||
return absl::StrCat("EG ", 1 + params_.N);
|
||||
case ModId::LFO:
|
||||
return absl::StrCat("LFO ", 1 + params_.N);
|
||||
|
||||
case ModId::Amplitude:
|
||||
return "Amplitude";
|
||||
case ModId::Pan:
|
||||
return "Pan";
|
||||
case ModId::Width:
|
||||
return "Width";
|
||||
case ModId::Position:
|
||||
return "Position";
|
||||
case ModId::Pitch:
|
||||
return "Pitch";
|
||||
case ModId::Volume:
|
||||
return "Volume";
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
|
||||
bool sfz::ModKey::operator==(const ModKey &other) const noexcept
|
||||
{
|
||||
return id_ == other.id_ && region_ == other.region_ &&
|
||||
parameters() == other.parameters();
|
||||
}
|
||||
|
||||
bool sfz::ModKey::operator!=(const ModKey &other) const noexcept
|
||||
{
|
||||
return !this->operator==(other);
|
||||
}
|
||||
78
src/sfizz/modulations/ModKey.h
Normal file
78
src/sfizz/modulations/ModKey.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// 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 "ModKeyHash.h"
|
||||
#include "../NumericId.h"
|
||||
#include <string>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
struct Region;
|
||||
|
||||
enum class ModId : int;
|
||||
|
||||
/**
|
||||
* @brief Identifier of a single modulation source or target within a SFZ instrument
|
||||
*/
|
||||
class ModKey {
|
||||
public:
|
||||
struct Parameters;
|
||||
|
||||
ModKey() = default;
|
||||
explicit ModKey(ModId id, NumericId<Region> region = {}, Parameters params = {})
|
||||
: id_(id), region_(region), params_(params) {}
|
||||
|
||||
static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step);
|
||||
static ModKey createNXYZ(ModId id, NumericId<Region> region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0);
|
||||
|
||||
explicit operator bool() const noexcept { return id_ != ModId(); }
|
||||
|
||||
const ModId& id() const noexcept { return id_; }
|
||||
NumericId<Region> region() const noexcept { return region_; }
|
||||
const Parameters& parameters() const noexcept { return params_; }
|
||||
|
||||
bool isSource() const noexcept;
|
||||
bool isTarget() const noexcept;
|
||||
int flags() const noexcept;
|
||||
std::string toString() const;
|
||||
|
||||
struct Parameters {
|
||||
Parameters() noexcept;
|
||||
Parameters(const Parameters& other) noexcept;
|
||||
Parameters& operator=(const Parameters& other) noexcept;
|
||||
|
||||
Parameters(Parameters&&) = delete;
|
||||
Parameters &operator=(Parameters&&) = delete;
|
||||
|
||||
bool operator==(const Parameters& other) const noexcept;
|
||||
bool operator!=(const Parameters& other) const noexcept;
|
||||
|
||||
union {
|
||||
//! Parameters if this key identifies a CC source
|
||||
struct { uint16_t cc; uint8_t curve, smooth; float value, step; };
|
||||
//! Parameters otherwise, based on the related opcode
|
||||
// eg. `N` in `lfoN`, `N, X` in `lfoN_eqX`
|
||||
struct { uint8_t N, X, Y, Z; };
|
||||
// !!! NOTE: NXYZ is expected to be stored in 0-indexed form
|
||||
// eg. `lfo1_eq2` is N=0, X=1
|
||||
};
|
||||
};
|
||||
|
||||
public:
|
||||
bool operator==(const ModKey &other) const noexcept;
|
||||
bool operator!=(const ModKey &other) const noexcept;
|
||||
|
||||
private:
|
||||
//! Identifier
|
||||
ModId id_ {};
|
||||
//! Region identifier, only applicable if the modulation is per-voice
|
||||
NumericId<Region> region_;
|
||||
//! List of values which identify the key uniquely, along with the hash and region
|
||||
Parameters params_ {};
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
34
src/sfizz/modulations/ModKeyHash.cpp
Normal file
34
src/sfizz/modulations/ModKeyHash.cpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// 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 "ModKeyHash.h"
|
||||
#include "ModKey.h"
|
||||
#include "ModId.h"
|
||||
#include "StringViewHelpers.h"
|
||||
#include <cstdint>
|
||||
|
||||
size_t std::hash<sfz::ModKey>::operator()(const sfz::ModKey &key) const
|
||||
{
|
||||
uint64_t k = hashNumber(static_cast<int>(key.id()));
|
||||
const sfz::ModKey::Parameters& p = key.parameters();
|
||||
|
||||
switch (key.id()) {
|
||||
case sfz::ModId::Controller:
|
||||
k = hashNumber(p.cc, k);
|
||||
k = hashNumber(p.curve, k);
|
||||
k = hashNumber(p.smooth, k);
|
||||
k = hashNumber(p.value, k);
|
||||
k = hashNumber(p.step, k);
|
||||
break;
|
||||
default:
|
||||
k = hashNumber(p.N, k);
|
||||
k = hashNumber(p.X, k);
|
||||
k = hashNumber(p.Y, k);
|
||||
k = hashNumber(p.Z, k);
|
||||
break;
|
||||
}
|
||||
return k;
|
||||
}
|
||||
17
src/sfizz/modulations/ModKeyHash.h
Normal file
17
src/sfizz/modulations/ModKeyHash.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// 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 <functional>
|
||||
#include <cstddef>
|
||||
|
||||
namespace sfz { class ModKey; }
|
||||
|
||||
namespace std {
|
||||
template <> struct hash<sfz::ModKey> {
|
||||
size_t operator()(const sfz::ModKey &key) const;
|
||||
};
|
||||
}
|
||||
431
src/sfizz/modulations/ModMatrix.cpp
Normal file
431
src/sfizz/modulations/ModMatrix.cpp
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
// 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 "ModMatrix.h"
|
||||
#include "ModId.h"
|
||||
#include "ModKey.h"
|
||||
#include "ModGenerator.h"
|
||||
#include "Buffer.h"
|
||||
#include "Config.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "Debug.h"
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <absl/strings/string_view.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
struct ModMatrix::Impl {
|
||||
double sampleRate_ {};
|
||||
uint32_t samplesPerBlock_ {};
|
||||
|
||||
uint32_t numFrames_ {};
|
||||
NumericId<Voice> currentVoiceId_ {};
|
||||
NumericId<Region> currentRegionId_ {};
|
||||
|
||||
struct Source {
|
||||
ModKey key;
|
||||
ModGenerator* gen {};
|
||||
bool bufferReady {};
|
||||
Buffer<float> buffer;
|
||||
};
|
||||
|
||||
struct ConnectionData {
|
||||
float sourceDepth_ {};
|
||||
};
|
||||
|
||||
struct Target {
|
||||
ModKey key;
|
||||
uint32_t region {};
|
||||
absl::flat_hash_map<uint32_t, ConnectionData> connectedSources;
|
||||
bool bufferReady {};
|
||||
Buffer<float> buffer;
|
||||
};
|
||||
|
||||
absl::flat_hash_map<ModKey, uint32_t> sourceIndex_;
|
||||
absl::flat_hash_map<ModKey, uint32_t> targetIndex_;
|
||||
|
||||
std::vector<Source> sources_;
|
||||
std::vector<Target> targets_;
|
||||
};
|
||||
|
||||
ModMatrix::ModMatrix()
|
||||
: impl_(new Impl)
|
||||
{
|
||||
setSampleRate(config::defaultSampleRate);
|
||||
setSamplesPerBlock(config::defaultSamplesPerBlock);
|
||||
}
|
||||
|
||||
ModMatrix::~ModMatrix()
|
||||
{
|
||||
}
|
||||
|
||||
void ModMatrix::clear()
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
impl.sourceIndex_.clear();
|
||||
impl.targetIndex_.clear();
|
||||
impl.sources_.clear();
|
||||
impl.targets_.clear();
|
||||
}
|
||||
|
||||
void ModMatrix::setSampleRate(double sampleRate)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
if (impl.sampleRate_ == sampleRate)
|
||||
return;
|
||||
|
||||
impl.sampleRate_ = sampleRate;
|
||||
|
||||
for (Impl::Source &source : impl.sources_)
|
||||
source.gen->setSampleRate(sampleRate);
|
||||
}
|
||||
|
||||
void ModMatrix::setSamplesPerBlock(unsigned samplesPerBlock)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
if (impl.samplesPerBlock_ == samplesPerBlock)
|
||||
return;
|
||||
|
||||
impl.samplesPerBlock_ = samplesPerBlock;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
source.buffer.resize(samplesPerBlock);
|
||||
source.gen->setSamplesPerBlock(samplesPerBlock);
|
||||
}
|
||||
for (Impl::Target &target : impl.targets_)
|
||||
target.buffer.resize(samplesPerBlock);
|
||||
}
|
||||
|
||||
ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& gen)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
auto it = impl.sourceIndex_.find(key);
|
||||
if (it != impl.sourceIndex_.end()) {
|
||||
ASSERT(&gen == impl.sources_[it->second].gen);
|
||||
return SourceId(it->second);
|
||||
}
|
||||
|
||||
SourceId id(static_cast<int>(impl.sources_.size()));
|
||||
impl.sources_.emplace_back();
|
||||
|
||||
Impl::Source &source = impl.sources_.back();
|
||||
source.key = key;
|
||||
source.gen = &gen;
|
||||
source.bufferReady = false;
|
||||
source.buffer.resize(impl.samplesPerBlock_);
|
||||
|
||||
impl.sourceIndex_[key] = id.number();
|
||||
|
||||
gen.setSampleRate(impl.sampleRate_);
|
||||
gen.setSamplesPerBlock(impl.samplesPerBlock_);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
auto it = impl.targetIndex_.find(key);
|
||||
if (it != impl.targetIndex_.end())
|
||||
return TargetId(it->second);
|
||||
|
||||
TargetId id(static_cast<int>(impl.targets_.size()));
|
||||
impl.targets_.emplace_back();
|
||||
|
||||
Impl::Target &target = impl.targets_.back();
|
||||
target.key = key;
|
||||
target.bufferReady = false;
|
||||
target.buffer.resize(impl.samplesPerBlock_);
|
||||
|
||||
impl.targetIndex_[key] = id.number();
|
||||
return id;
|
||||
}
|
||||
|
||||
ModMatrix::SourceId ModMatrix::findSource(const ModKey& key)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
auto it = impl.sourceIndex_.find(key);
|
||||
if (it == impl.sourceIndex_.end())
|
||||
return {};
|
||||
|
||||
return SourceId(it->second);
|
||||
}
|
||||
|
||||
ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
auto it = impl.targetIndex_.find(key);
|
||||
if (it == impl.targetIndex_.end())
|
||||
return {};
|
||||
|
||||
return TargetId(it->second);
|
||||
}
|
||||
|
||||
bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
unsigned sourceIndex = sourceId.number();
|
||||
unsigned targetIndex = targetId.number();
|
||||
|
||||
if (sourceIndex >= impl.sources_.size() || targetIndex >= impl.targets_.size())
|
||||
return false;
|
||||
|
||||
Impl::Target& target = impl.targets_[targetIndex];
|
||||
Impl::ConnectionData& conn = target.connectedSources[sourceIndex];
|
||||
conn.sourceDepth_ = sourceDepth;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModMatrix::init()
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
const int flags = source.key.flags();
|
||||
if (flags & kModIsPerCycle)
|
||||
source.gen->init(source.key, {});
|
||||
}
|
||||
}
|
||||
|
||||
void ModMatrix::initVoice(NumericId<Voice> voiceId, NumericId<Region> regionId)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
const int flags = source.key.flags();
|
||||
if ((flags & kModIsPerVoice) && source.key.region() == regionId)
|
||||
source.gen->init(source.key, voiceId);
|
||||
}
|
||||
}
|
||||
|
||||
void ModMatrix::beginCycle(unsigned numFrames)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
impl.numFrames_ = numFrames;
|
||||
|
||||
for (Impl::Source &source : impl.sources_)
|
||||
source.bufferReady = false;
|
||||
for (Impl::Target &target : impl.targets_)
|
||||
target.bufferReady = false;
|
||||
}
|
||||
|
||||
void ModMatrix::endCycle()
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
const uint32_t numFrames = impl.numFrames_;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
if (!source.bufferReady) {
|
||||
const int flags = source.key.flags();
|
||||
if (flags & kModIsPerCycle) {
|
||||
absl::Span<float> buffer(source.buffer.data(), numFrames);
|
||||
source.gen->generateDiscarded(source.key, {}, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl.numFrames_ = 0;
|
||||
}
|
||||
|
||||
void ModMatrix::beginVoice(NumericId<Voice> voiceId, NumericId<Region> regionId)
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
||||
impl.currentVoiceId_ = voiceId;
|
||||
impl.currentRegionId_ = regionId;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
const int flags = source.key.flags();
|
||||
if (flags & kModIsPerVoice)
|
||||
source.bufferReady = false;
|
||||
}
|
||||
for (Impl::Target &target : impl.targets_) {
|
||||
const int flags = target.key.flags();
|
||||
if (flags & kModIsPerVoice)
|
||||
target.bufferReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ModMatrix::endVoice()
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
const uint32_t numFrames = impl.numFrames_;
|
||||
const NumericId<Voice> voiceId = impl.currentVoiceId_;
|
||||
const NumericId<Region> regionId = impl.currentRegionId_;
|
||||
|
||||
for (Impl::Source &source : impl.sources_) {
|
||||
if (!source.bufferReady) {
|
||||
const int flags = source.key.flags();
|
||||
if ((flags & kModIsPerVoice) && source.key.region() == regionId) {
|
||||
absl::Span<float> buffer(source.buffer.data(), numFrames);
|
||||
source.gen->generateDiscarded(source.key, voiceId, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl.currentVoiceId_ = {};
|
||||
impl.currentRegionId_ = {};
|
||||
}
|
||||
|
||||
float* ModMatrix::getModulation(TargetId targetId)
|
||||
{
|
||||
if (!validTarget(targetId))
|
||||
return nullptr;
|
||||
|
||||
Impl& impl = *impl_;
|
||||
const NumericId<Region> regionId = impl.currentRegionId_;
|
||||
const uint32_t targetIndex = targetId.number();
|
||||
Impl::Target &target = impl.targets_[targetIndex];
|
||||
const int targetFlags = target.key.flags();
|
||||
|
||||
const uint32_t numFrames = impl.numFrames_;
|
||||
absl::Span<float> buffer(target.buffer.data(), numFrames);
|
||||
|
||||
// only accept per-voice targets of the same region
|
||||
if ((targetFlags & kModIsPerVoice) && regionId != target.key.region())
|
||||
return nullptr;
|
||||
|
||||
// check if already processed
|
||||
if (target.bufferReady)
|
||||
return buffer.data();
|
||||
|
||||
// set the ready flag to prevent a cycle
|
||||
// in case there is, be sure to initialize the buffer
|
||||
target.bufferReady = true;
|
||||
|
||||
auto sourcesPos = target.connectedSources.begin();
|
||||
auto sourcesEnd = target.connectedSources.end();
|
||||
bool isFirstSource = true;
|
||||
|
||||
// generate sources in their dedicated buffers
|
||||
// then add or multiply, depending on target flags
|
||||
while (sourcesPos != sourcesEnd) {
|
||||
Impl::Source &source = impl.sources_[sourcesPos->first];
|
||||
const float sourceDepth = sourcesPos->second.sourceDepth_;
|
||||
const int sourceFlags = source.key.flags();
|
||||
|
||||
// only accept per-voice sources of the same region
|
||||
bool useThisSource = true;
|
||||
if (sourceFlags & kModIsPerVoice)
|
||||
useThisSource = (regionId == source.key.region());
|
||||
|
||||
if (useThisSource) {
|
||||
absl::Span<float> sourceBuffer(source.buffer.data(), numFrames);
|
||||
|
||||
// unless source is already done, process it
|
||||
if (!source.bufferReady) {
|
||||
source.gen->generate(source.key, impl.currentVoiceId_, sourceBuffer);
|
||||
source.bufferReady = true;
|
||||
}
|
||||
|
||||
if (isFirstSource) {
|
||||
if (sourceDepth != 1) {
|
||||
for (uint32_t i = 0; i < numFrames; ++i)
|
||||
buffer[i] = sourceDepth * sourceBuffer[i];
|
||||
}
|
||||
else {
|
||||
copy(absl::Span<const float>(sourceBuffer), buffer);
|
||||
}
|
||||
isFirstSource = false;
|
||||
}
|
||||
else {
|
||||
if (targetFlags & kModIsMultiplicative) {
|
||||
for (uint32_t i = 0; i < numFrames; ++i)
|
||||
buffer[i] *= sourceDepth * sourceBuffer[i];
|
||||
}
|
||||
else if (targetFlags & kModIsPercentMultiplicative) {
|
||||
for (uint32_t i = 0; i < numFrames; ++i)
|
||||
buffer[i] *= (0.01f * sourceDepth) * sourceBuffer[i];
|
||||
}
|
||||
else {
|
||||
ASSERT(targetFlags & kModIsAdditive);
|
||||
for (uint32_t i = 0; i < numFrames; ++i)
|
||||
buffer[i] += sourceDepth * sourceBuffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++sourcesPos;
|
||||
}
|
||||
|
||||
// if there were no source, fill output with the neutral element
|
||||
if (isFirstSource) {
|
||||
if (targetFlags & kModIsMultiplicative)
|
||||
fill(buffer, 1.0f);
|
||||
else if (targetFlags & kModIsPercentMultiplicative)
|
||||
fill(buffer, 100.0f);
|
||||
else {
|
||||
ASSERT(targetFlags & kModIsAdditive);
|
||||
fill(buffer, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
bool ModMatrix::validTarget(TargetId id) const
|
||||
{
|
||||
return static_cast<unsigned>(id.number()) < impl_->targets_.size();
|
||||
}
|
||||
|
||||
bool ModMatrix::validSource(SourceId id) const
|
||||
{
|
||||
return static_cast<unsigned>(id.number()) < impl_->sources_.size();
|
||||
}
|
||||
|
||||
std::string ModMatrix::toDotGraph() const
|
||||
{
|
||||
const Impl& impl = *impl_;
|
||||
|
||||
struct Edge {
|
||||
std::string source;
|
||||
std::string target;
|
||||
};
|
||||
|
||||
// collect all connections as string pairs
|
||||
std::vector<Edge> edges;
|
||||
for (const Impl::Target& target : impl.targets_) {
|
||||
for (const auto& cs : target.connectedSources) {
|
||||
const Impl::Source& source = impl.sources_[cs.first];
|
||||
Edge e;
|
||||
e.source = source.key.toString();
|
||||
e.target = target.key.toString();
|
||||
edges.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
|
||||
// alphabetic sort, to produce stable output for unit testing
|
||||
auto compare = [](const Edge& a, const Edge& b) -> bool {
|
||||
std::pair<absl::string_view, absl::string_view> aa{a.source, a.target};
|
||||
std::pair<absl::string_view, absl::string_view> bb{b.source, b.target};
|
||||
return aa < bb;
|
||||
};
|
||||
std::sort(edges.begin(), edges.end(), compare);
|
||||
|
||||
// write dot graph
|
||||
std::string dot;
|
||||
dot.reserve(1024);
|
||||
absl::StrAppend(&dot, "digraph {" "\n");
|
||||
for (const Edge& e : edges) {
|
||||
absl::StrAppend(&dot, "\t" "\"", e.source, "\""
|
||||
" -> " "\"", e.target, "\"" "\n");
|
||||
}
|
||||
absl::StrAppend(&dot, "}" "\n");
|
||||
return dot;
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
181
src/sfizz/modulations/ModMatrix.h
Normal file
181
src/sfizz/modulations/ModMatrix.h
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
// 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 "../NumericId.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
class ModKey;
|
||||
class ModGenerator;
|
||||
class Voice;
|
||||
struct Region;
|
||||
|
||||
/**
|
||||
* @brief Modulation matrix
|
||||
*/
|
||||
class ModMatrix {
|
||||
public:
|
||||
ModMatrix();
|
||||
~ModMatrix();
|
||||
|
||||
struct SourceIdTag;
|
||||
struct TargetIdTag;
|
||||
|
||||
//! Identifier of a modulation source
|
||||
typedef NumericId<SourceIdTag> SourceId;
|
||||
|
||||
//! Identifier of a modulation target
|
||||
typedef NumericId<TargetIdTag> TargetId;
|
||||
|
||||
/**
|
||||
* @brief Reset the matrix to the empty state.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Change the sample rate.
|
||||
*
|
||||
* @param sampleRate new sample rate
|
||||
*/
|
||||
void setSampleRate(double sampleRate);
|
||||
|
||||
/**
|
||||
* @brief Resize the modulation buffers.
|
||||
*
|
||||
* @param samplesPerBlock new block size
|
||||
*/
|
||||
void setSamplesPerBlock(unsigned samplesPerBlock);
|
||||
|
||||
/**
|
||||
* @brief Register a modulation source inside the matrix.
|
||||
* If it is already present, it just returns the existing id.
|
||||
*
|
||||
* @param key source key
|
||||
* @param gen generator
|
||||
* @param flags source flags
|
||||
*/
|
||||
SourceId registerSource(const ModKey& key, ModGenerator& gen);
|
||||
|
||||
/**
|
||||
* @brief Register a modulation target inside the matrix.
|
||||
*
|
||||
* @param key target key
|
||||
* @param region target region
|
||||
* @param flags target flags
|
||||
*/
|
||||
TargetId registerTarget(const ModKey& key);
|
||||
|
||||
/**
|
||||
* @brief Look up a source by key.
|
||||
*
|
||||
* @param key source key
|
||||
*/
|
||||
SourceId findSource(const ModKey& key);
|
||||
|
||||
/**
|
||||
* @brief Look up a target by key.
|
||||
*
|
||||
* @param key target key
|
||||
*/
|
||||
TargetId findTarget(const ModKey& key);
|
||||
|
||||
/**
|
||||
* @brief Connect a source and a destination inside the matrix.
|
||||
*
|
||||
* @param sourceId source of the connection
|
||||
* @param targetId target of the connection
|
||||
* @param sourceDepth amount which multiplies the source output
|
||||
* @return true if the connection was successfully made, otherwise false
|
||||
*/
|
||||
bool connect(SourceId sourceId, TargetId targetId, float sourceDepth);
|
||||
|
||||
/**
|
||||
* @brief Reinitialize modulation sources overall.
|
||||
* This must be called once after setting up the matrix.
|
||||
*/
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief Reinitialize modulation source for a given voice.
|
||||
* This must be called first after a voice enters active state.
|
||||
*/
|
||||
void initVoice(NumericId<Voice> voiceId, NumericId<Region> regionId);
|
||||
|
||||
/**
|
||||
* @brief Start modulation processing for the entire cycle.
|
||||
* This clears all the buffers.
|
||||
*
|
||||
* @param numFrames
|
||||
*/
|
||||
void beginCycle(unsigned numFrames);
|
||||
|
||||
/**
|
||||
* @brief End modulation processing for the entire cycle.
|
||||
* This performs a dummy run of any unused modulations.
|
||||
*/
|
||||
void endCycle();
|
||||
|
||||
/**
|
||||
* @brief Start modulation processing for a given voice.
|
||||
* This clears all the buffers which are per-voice.
|
||||
*
|
||||
* @param voiceId the identifier of the current voice
|
||||
* @param regionId the identifier of the region of the current voice
|
||||
*/
|
||||
void beginVoice(NumericId<Voice> voiceId, NumericId<Region> regionId);
|
||||
|
||||
/**
|
||||
* @brief End modulation processing for a given voice.
|
||||
* This performs a dummy run of any unused modulations which are per-cycle.
|
||||
*/
|
||||
void endVoice();
|
||||
|
||||
/**
|
||||
* @brief Get the modulation buffer for the given target.
|
||||
* If the target does not exist, the result is null.
|
||||
*
|
||||
* @param targetId identifier of the modulation target
|
||||
*/
|
||||
float* getModulation(TargetId targetId);
|
||||
|
||||
/**
|
||||
* @brief Get the modulation buffer for the given target.
|
||||
* Same as `getModulation`, but accepting a key directly.
|
||||
*
|
||||
* @param targetKey key of the modulation target
|
||||
*/
|
||||
float* getModulationByKey(const ModKey& targetKey)
|
||||
{ return getModulation(findTarget(targetKey)); }
|
||||
|
||||
/**
|
||||
* @brief Return whether the target identifier is valid.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
bool validTarget(TargetId id) const;
|
||||
|
||||
/**
|
||||
* @brief Return whether the source identifier is valid.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
bool validSource(SourceId id) const;
|
||||
|
||||
/**
|
||||
* @brief Get a representation of the matrix written as a Dot graph.
|
||||
*/
|
||||
std::string toDotGraph() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
94
src/sfizz/modulations/sources/Controller.cpp
Normal file
94
src/sfizz/modulations/sources/Controller.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// 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 "Controller.h"
|
||||
#include "../ModKey.h"
|
||||
#include "../../Smoothers.h"
|
||||
#include "../../ModifierHelpers.h"
|
||||
#include "../../Resources.h"
|
||||
#include "../../Config.h"
|
||||
#include "../../Debug.h"
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
struct ControllerSource::Impl {
|
||||
double sampleRate_ = config::defaultSampleRate;
|
||||
Resources* res_ = nullptr;
|
||||
absl::flat_hash_map<ModKey, Smoother> smoother_;
|
||||
};
|
||||
|
||||
ControllerSource::ControllerSource(Resources& res)
|
||||
: impl_(new Impl)
|
||||
{
|
||||
impl_->res_ = &res;
|
||||
}
|
||||
|
||||
ControllerSource::~ControllerSource()
|
||||
{
|
||||
}
|
||||
|
||||
void ControllerSource::setSampleRate(double sampleRate)
|
||||
{
|
||||
if (impl_->sampleRate_ == sampleRate)
|
||||
return;
|
||||
|
||||
impl_->sampleRate_ = sampleRate;
|
||||
|
||||
for (auto& item : impl_->smoother_) {
|
||||
const ModKey::Parameters p = item.first.parameters();
|
||||
item.second.setSmoothing(p.smooth, sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerSource::setSamplesPerBlock(unsigned count)
|
||||
{
|
||||
(void)count;
|
||||
}
|
||||
|
||||
void ControllerSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId)
|
||||
{
|
||||
(void)voiceId;
|
||||
|
||||
const ModKey::Parameters p = sourceKey.parameters();
|
||||
if (p.smooth > 0) {
|
||||
Smoother s;
|
||||
s.setSmoothing(p.smooth, impl_->sampleRate_);
|
||||
impl_->smoother_[sourceKey] = s;
|
||||
}
|
||||
else {
|
||||
impl_->smoother_.erase(sourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
|
||||
{
|
||||
(void)voiceId;
|
||||
|
||||
const ModKey::Parameters p = sourceKey.parameters();
|
||||
const Resources& res = *impl_->res_;
|
||||
const Curve& curve = res.curves.getCurve(p.curve);
|
||||
const MidiState& ms = res.midiState;
|
||||
const EventVector& events = ms.getCCEvents(p.cc);
|
||||
|
||||
auto transformValue = [p, &curve](float x) {
|
||||
return curve.evalNormalized(x) * p.value;
|
||||
};
|
||||
|
||||
if (p.step > 0.0f)
|
||||
linearEnvelope(events, buffer, transformValue, p.step);
|
||||
else
|
||||
linearEnvelope(events, buffer, transformValue);
|
||||
|
||||
auto it = impl_->smoother_.find(sourceKey);
|
||||
if (it != impl_->smoother_.end()) {
|
||||
Smoother& s = it->second;
|
||||
bool canShortcut = events.size() == 1;
|
||||
s.process(buffer, buffer, canShortcut);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
29
src/sfizz/modulations/sources/Controller.h
Normal file
29
src/sfizz/modulations/sources/Controller.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// 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"
|
||||
#include <memory>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
struct Resources;
|
||||
|
||||
class ControllerSource : public ModGenerator {
|
||||
public:
|
||||
explicit ControllerSource(Resources& res);
|
||||
~ControllerSource();
|
||||
void setSampleRate(double sampleRate) override;
|
||||
void setSamplesPerBlock(unsigned count) override;
|
||||
void init(const ModKey& sourceKey, NumericId<Voice> voiceId) override;
|
||||
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
|
@ -5,6 +5,8 @@ project(sfizz)
|
|||
|
||||
set(SFIZZ_TEST_SOURCES
|
||||
RegionT.cpp
|
||||
RegionTHelpers.h
|
||||
RegionTHelpers.cpp
|
||||
ParsingT.cpp
|
||||
HelpersT.cpp
|
||||
HelpersT.cpp
|
||||
|
|
@ -36,6 +38,7 @@ set(SFIZZ_TEST_SOURCES
|
|||
SwapAndPopT.cpp
|
||||
TuningT.cpp
|
||||
ConcurrencyT.cpp
|
||||
ModulationsT.cpp
|
||||
)
|
||||
|
||||
add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES})
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps")
|
|||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
||||
#if 0
|
||||
TEST_CASE("[linearModifiers] Compare with envelopes")
|
||||
{
|
||||
sfz::Resources resources;
|
||||
|
|
@ -360,4 +361,4 @@ TEST_CASE("[multiplicativeModifiers] Compare with envelopes")
|
|||
});
|
||||
REQUIRE(approxEqual<float>(output, envelope));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
// 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 "RegionTHelpers.h"
|
||||
#include "sfizz/Synth.h"
|
||||
#include "sfizz/SfzHelpers.h"
|
||||
#include "sfizz/modulations/ModId.h"
|
||||
#include "sfizz/modulations/ModKey.h"
|
||||
#include "catch2/catch.hpp"
|
||||
#include "ghc/fs_std.hpp"
|
||||
#if defined(__APPLE__)
|
||||
|
|
@ -356,9 +359,11 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines")
|
|||
|
||||
REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 );
|
||||
REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 );
|
||||
REQUIRE(!synth.getRegionView(2)->modifiers[Mod::amplitude].empty());
|
||||
REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].contains(10));
|
||||
REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].getWithDefault(10).value == 34.0f);
|
||||
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Amplitude, synth.getRegionView(2)->getId());
|
||||
const RegionCCView view(*synth.getRegionView(2), target);
|
||||
REQUIRE(!view.empty());
|
||||
REQUIRE(view.at(10).value == 34.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[Files] Specific bug: relative path with backslashes")
|
||||
|
|
|
|||
100
tests/ModulationsT.cpp
Normal file
100
tests/ModulationsT.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// 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/modulations/ModId.h"
|
||||
#include "sfizz/modulations/ModKey.h"
|
||||
#include "sfizz/Synth.h"
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
TEST_CASE("[Modulations] Identifiers")
|
||||
{
|
||||
// check that modulations are well defined as either source and target
|
||||
// and all targets have their default value defined
|
||||
|
||||
sfz::ModIds::forEachSourceId([](sfz::ModId id)
|
||||
{
|
||||
REQUIRE(sfz::ModIds::isSource(id));
|
||||
REQUIRE(!sfz::ModIds::isTarget(id));
|
||||
});
|
||||
|
||||
sfz::ModIds::forEachTargetId([](sfz::ModId id)
|
||||
{
|
||||
REQUIRE(sfz::ModIds::isTarget(id));
|
||||
REQUIRE(!sfz::ModIds::isSource(id));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("[Modulations] Flags")
|
||||
{
|
||||
// check validity of modulation flags
|
||||
|
||||
static auto* checkBasicFlags = +[](int flags)
|
||||
{
|
||||
REQUIRE(flags != sfz::kModFlagsInvalid);
|
||||
REQUIRE((bool(flags & sfz::kModIsPerCycle) +
|
||||
bool(flags & sfz::kModIsPerVoice)) == 1);
|
||||
};
|
||||
static auto* checkSourceFlags = +[](int flags)
|
||||
{
|
||||
checkBasicFlags(flags);
|
||||
REQUIRE((bool(flags & sfz::kModIsAdditive) +
|
||||
bool(flags & sfz::kModIsMultiplicative) +
|
||||
bool(flags & sfz::kModIsPercentMultiplicative)) == 0);
|
||||
};
|
||||
static auto* checkTargetFlags = +[](int flags)
|
||||
{
|
||||
checkBasicFlags(flags);
|
||||
REQUIRE((bool(flags & sfz::kModIsAdditive) +
|
||||
bool(flags & sfz::kModIsMultiplicative) +
|
||||
bool(flags & sfz::kModIsPercentMultiplicative)) == 1);
|
||||
};
|
||||
|
||||
sfz::ModIds::forEachSourceId([](sfz::ModId id)
|
||||
{
|
||||
checkSourceFlags(sfz::ModIds::flags(id));
|
||||
});
|
||||
|
||||
sfz::ModIds::forEachTargetId([](sfz::ModId id)
|
||||
{
|
||||
checkTargetFlags(sfz::ModIds::flags(id));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("[Modulations] Display names")
|
||||
{
|
||||
// check all modulations are implemented in `toString`
|
||||
|
||||
sfz::ModIds::forEachSourceId([](sfz::ModId id)
|
||||
{
|
||||
REQUIRE(!sfz::ModKey(id).toString().empty());
|
||||
});
|
||||
|
||||
sfz::ModIds::forEachTargetId([](sfz::ModId id)
|
||||
{
|
||||
REQUIRE(!sfz::ModKey(id).toString().empty());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("[Modulations] Connection graph from SFZ")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.loadSfzString("/modulation.sfz", R"(
|
||||
<region>
|
||||
sample=*sine
|
||||
amplitude_oncc20=59 amplitude_curvecc20=3
|
||||
pitch_oncc42=71 pitch_smoothcc42=32
|
||||
pan_oncc36=14.5 pan_stepcc36=1.5
|
||||
width_oncc425=29
|
||||
)");
|
||||
const std::string graph = synth.getResources().modMatrix.toDotGraph();
|
||||
REQUIRE(graph == R"(digraph {
|
||||
"Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude"
|
||||
"Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan"
|
||||
"Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch"
|
||||
"Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width"
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
|
@ -4,10 +4,14 @@
|
|||
// 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 "RegionTHelpers.h"
|
||||
#include "sfizz/MidiState.h"
|
||||
#include "sfizz/Region.h"
|
||||
#include "sfizz/SfzHelpers.h"
|
||||
#include "sfizz/modulations/ModId.h"
|
||||
#include "sfizz/modulations/ModKey.h"
|
||||
#include "catch2/catch.hpp"
|
||||
#include <stdexcept>
|
||||
using namespace Catch::literals;
|
||||
using namespace sfz::literals;
|
||||
using namespace sfz;
|
||||
|
|
@ -541,28 +545,29 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
|
||||
SECTION("pan_oncc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::pan].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Pan, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "pan_oncc45", "4.2" });
|
||||
REQUIRE(region.modifiers[Mod::pan].contains(45));
|
||||
REQUIRE(region.modifiers[Mod::pan][45].value == 4.2_a);
|
||||
REQUIRE(view.at(45).value == 4.2_a);
|
||||
region.parseOpcode({ "pan_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::pan][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "pan_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pan][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "pan_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pan][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "pan_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::pan][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "pan_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pan][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "pan_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pan][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "pan_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::pan][120].step == 24.0_a);
|
||||
REQUIRE(view.at(120).step == 24.0_a);
|
||||
region.parseOpcode({ "pan_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pan][120].step == 200.0_a);
|
||||
REQUIRE(view.at(120).step == 200.0_a);
|
||||
region.parseOpcode({ "pan_stepcc120", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pan][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
|
||||
SECTION("width")
|
||||
|
|
@ -580,28 +585,29 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
|
||||
SECTION("width_oncc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::width].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Width, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "width_oncc45", "4.2" });
|
||||
REQUIRE(region.modifiers[Mod::width].contains(45));
|
||||
REQUIRE(region.modifiers[Mod::width][45].value == 4.2_a);
|
||||
REQUIRE(view.at(45).value == 4.2_a);
|
||||
region.parseOpcode({ "width_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::width][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "width_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::width][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "width_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::width][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "width_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::width][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "width_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::width][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "width_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::width][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "width_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::width][120].step == 24.0_a);
|
||||
REQUIRE(view.at(120).step == 24.0_a);
|
||||
region.parseOpcode({ "width_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::width][120].step == 200.0_a);
|
||||
REQUIRE(view.at(120).step == 200.0_a);
|
||||
region.parseOpcode({ "width_stepcc120", "-20" });
|
||||
REQUIRE(region.modifiers[Mod::width][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
|
||||
SECTION("position")
|
||||
|
|
@ -619,28 +625,29 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
|
||||
SECTION("position_oncc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::position].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Position, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "position_oncc45", "4.2" });
|
||||
REQUIRE(region.modifiers[Mod::position].contains(45));
|
||||
REQUIRE(region.modifiers[Mod::position][45].value == 4.2_a);
|
||||
REQUIRE(view.at(45).value == 4.2_a);
|
||||
region.parseOpcode({ "position_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::position][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "position_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::position][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "position_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::position][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "position_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::position][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "position_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::position][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "position_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::position][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "position_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::position][120].step == 24.0_a);
|
||||
REQUIRE(view.at(120).step == 24.0_a);
|
||||
region.parseOpcode({ "position_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::position][120].step == 200.0_a);
|
||||
REQUIRE(view.at(120).step == 200.0_a);
|
||||
region.parseOpcode({ "position_stepcc120", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::position][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
|
||||
SECTION("amp_keycenter")
|
||||
|
|
@ -1641,95 +1648,93 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
|
||||
SECTION("amplitude_cc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::amplitude].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Amplitude, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "amplitude_cc1", "40" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude].contains(1));
|
||||
REQUIRE(region.modifiers[Mod::amplitude][1].value == 40.0_a);
|
||||
REQUIRE(view.at(1).value == 40.0_a);
|
||||
region.parseOpcode({ "amplitude_oncc2", "30" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude].contains(2));
|
||||
REQUIRE(region.modifiers[Mod::amplitude][2].value == 30.0_a);
|
||||
REQUIRE(view.at(2).value == 30.0_a);
|
||||
region.parseOpcode({ "amplitude_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "amplitude_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "amplitude_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "amplitude_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "amplitude_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "amplitude_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "amplitude_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][120].step == 24.0_a);
|
||||
REQUIRE(view.at(120).step == 24.0_a);
|
||||
region.parseOpcode({ "amplitude_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][120].step == 100.0_a);
|
||||
REQUIRE(view.at(120).step == 100.0_a);
|
||||
region.parseOpcode({ "amplitude_stepcc120", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::amplitude][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
|
||||
SECTION("volume_oncc/gain_cc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::volume].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Volume, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "gain_cc1", "40" });
|
||||
REQUIRE(region.modifiers[Mod::volume].contains(1));
|
||||
REQUIRE(region.modifiers[Mod::volume][1].value == 40_a);
|
||||
REQUIRE(view.at(1).value == 40_a);
|
||||
region.parseOpcode({ "volume_oncc2", "-76" });
|
||||
REQUIRE(region.modifiers[Mod::volume].contains(2));
|
||||
REQUIRE(region.modifiers[Mod::volume][2].value == -76.0_a);
|
||||
REQUIRE(view.at(2).value == -76.0_a);
|
||||
region.parseOpcode({ "gain_oncc4", "-1" });
|
||||
REQUIRE(region.modifiers[Mod::volume].contains(4));
|
||||
REQUIRE(region.modifiers[Mod::volume][4].value == -1.0_a);
|
||||
REQUIRE(view.at(4).value == -1.0_a);
|
||||
region.parseOpcode({ "volume_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::volume][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "volume_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::volume][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "volume_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::volume][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "volume_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::volume][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "volume_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::volume][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "volume_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::volume][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "volume_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::volume][120].step == 24.0f);
|
||||
REQUIRE(view.at(120).step == 24.0f);
|
||||
region.parseOpcode({ "volume_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::volume][120].step == 144.0f);
|
||||
REQUIRE(view.at(120).step == 144.0f);
|
||||
region.parseOpcode({ "volume_stepcc120", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::volume][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
|
||||
SECTION("tune_cc/pitch_cc")
|
||||
{
|
||||
REQUIRE(region.modifiers[Mod::pitch].empty());
|
||||
const ModKey target = ModKey::createNXYZ(ModId::Pitch, region.getId());
|
||||
const RegionCCView view(region, target);
|
||||
REQUIRE(view.empty());
|
||||
region.parseOpcode({ "pitch_cc1", "40" });
|
||||
REQUIRE(region.modifiers[Mod::pitch].contains(1));
|
||||
REQUIRE(region.modifiers[Mod::pitch][1].value == 40.0);
|
||||
REQUIRE(view.at(1).value == 40.0);
|
||||
region.parseOpcode({ "tune_oncc2", "-76" });
|
||||
REQUIRE(region.modifiers[Mod::pitch].contains(2));
|
||||
REQUIRE(region.modifiers[Mod::pitch][2].value == -76.0);
|
||||
REQUIRE(view.at(2).value == -76.0);
|
||||
region.parseOpcode({ "pitch_oncc4", "-1" });
|
||||
REQUIRE(region.modifiers[Mod::pitch].contains(4));
|
||||
REQUIRE(region.modifiers[Mod::pitch][4].value == -1.0);
|
||||
REQUIRE(view.at(4).value == -1.0);
|
||||
region.parseOpcode({ "tune_curvecc17", "18" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][17].curve == 18);
|
||||
REQUIRE(view.at(17).curve == 18);
|
||||
region.parseOpcode({ "pitch_curvecc17", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][17].curve == 255);
|
||||
REQUIRE(view.at(17).curve == 255);
|
||||
region.parseOpcode({ "tune_curvecc17", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][17].curve == 0);
|
||||
REQUIRE(view.at(17).curve == 0);
|
||||
region.parseOpcode({ "pitch_smoothcc14", "85" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][14].smooth == 85);
|
||||
REQUIRE(view.at(14).smooth == 85);
|
||||
region.parseOpcode({ "tune_smoothcc14", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][14].smooth == 100);
|
||||
REQUIRE(view.at(14).smooth == 100);
|
||||
region.parseOpcode({ "pitch_smoothcc14", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][14].smooth == 0);
|
||||
REQUIRE(view.at(14).smooth == 0);
|
||||
region.parseOpcode({ "tune_stepcc120", "24" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][120].step == 24.0f);
|
||||
REQUIRE(view.at(120).step == 24.0f);
|
||||
region.parseOpcode({ "pitch_stepcc120", "15482" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][120].step == 9600.0f);
|
||||
REQUIRE(view.at(120).step == 9600.0f);
|
||||
region.parseOpcode({ "tune_stepcc120", "-2" });
|
||||
REQUIRE(region.modifiers[Mod::pitch][120].step == 0.0f);
|
||||
REQUIRE(view.at(120).step == 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
tests/RegionTHelpers.cpp
Normal file
41
tests/RegionTHelpers.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// 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 "RegionTHelpers.h"
|
||||
#include "sfizz/modulations/ModId.h"
|
||||
|
||||
size_t RegionCCView::size() const
|
||||
{
|
||||
size_t count = 0;
|
||||
for (const sfz::Region::Connection& conn : region_.connections)
|
||||
count += match(conn);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool RegionCCView::empty() const
|
||||
{
|
||||
for (const sfz::Region::Connection& conn : region_.connections)
|
||||
if (match(conn))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
sfz::ModKey::Parameters RegionCCView::at(int cc) const
|
||||
{
|
||||
for (const sfz::Region::Connection& conn : region_.connections) {
|
||||
if (match(conn)) {
|
||||
const sfz::ModKey::Parameters p = conn.source.parameters();
|
||||
if (p.cc == cc)
|
||||
return p;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("Region CC");
|
||||
}
|
||||
|
||||
bool RegionCCView::match(const sfz::Region::Connection& conn) const
|
||||
{
|
||||
return conn.source.id() == sfz::ModId::Controller && conn.target == target_;
|
||||
}
|
||||
28
tests/RegionTHelpers.h
Normal file
28
tests/RegionTHelpers.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// 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 "sfizz/Region.h"
|
||||
#include "sfizz/modulations/ModKey.h"
|
||||
|
||||
class RegionCCView {
|
||||
public:
|
||||
RegionCCView(const sfz::Region& region, sfz::ModKey target)
|
||||
: region_(region), target_(target)
|
||||
{
|
||||
}
|
||||
|
||||
size_t size() const;
|
||||
bool empty() const;
|
||||
sfz::ModKey::Parameters at(int cc) const;
|
||||
|
||||
private:
|
||||
bool match(const sfz::Region::Connection& conn) const;
|
||||
|
||||
private:
|
||||
const sfz::Region& region_;
|
||||
sfz::ModKey target_;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue