Merge pull request #335 from jpcima/mm

Add modulation matrix
This commit is contained in:
JP Cimalando 2020-08-09 21:28:46 +02:00 committed by GitHub
commit a12cf4e24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1661 additions and 403 deletions

5
dpf.mk
View file

@ -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 \

View file

@ -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

View file

@ -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

View file

@ -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
}};
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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:

View file

@ -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();
}
};
}

View file

@ -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 };

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View 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

View 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

View 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

View 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);
}

View 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

View 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;
}

View 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;
};
}

View 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

View 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

View 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

View 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

View file

@ -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})

View file

@ -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

View file

@ -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
View 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"
}
)");
}

View file

@ -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
View 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
View 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_;
};