Add modulation matrix

This commit is contained in:
Jean Pierre Cimalando 2020-07-26 19:25:59 +02:00
parent fe077d56a9
commit 675d7f3fc5
21 changed files with 1291 additions and 57 deletions

5
dpf.mk
View file

@ -60,6 +60,11 @@ SFIZZ_SOURCES = \
src/sfizz/Curve.cpp \ src/sfizz/Curve.cpp \
src/sfizz/effects/Apan.cpp \ src/sfizz/effects/Apan.cpp \
src/sfizz/Effects.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/Compressor.cpp \
src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Disto.cpp \
src/sfizz/effects/Eq.cpp \ src/sfizz/effects/Eq.cpp \

View file

@ -29,6 +29,12 @@ set (SFIZZ_HEADERS
sfizz/Debug.h sfizz/Debug.h
sfizz/utility/SpinMutex.h sfizz/utility/SpinMutex.h
sfizz/utility/SpinMutex.cpp 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/ResonantArray.h
sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArrayAVX.h
sfizz/effects/impl/ResonantArraySSE.h sfizz/effects/impl/ResonantArraySSE.h
@ -128,6 +134,11 @@ set (SFIZZ_SOURCES
sfizz/RTSemaphore.cpp sfizz/RTSemaphore.cpp
sfizz/Panning.cpp sfizz/Panning.cpp
sfizz/Effects.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/Nothing.cpp
sfizz/effects/Filter.cpp sfizz/effects/Filter.cpp
sfizz/effects/Eq.cpp sfizz/effects/Eq.cpp

View file

@ -11,6 +11,7 @@
#include "Opcode.h" #include "Opcode.h"
#include "StringViewHelpers.h" #include "StringViewHelpers.h"
#include "ModifierHelpers.h" #include "ModifierHelpers.h"
#include "modulations/ModId.h"
#include "absl/strings/str_replace.h" #include "absl/strings/str_replace.h"
#include "absl/strings/str_cat.h" #include "absl/strings/str_cat.h"
#include "absl/algorithm/container.h" #include "absl/algorithm/container.h"
@ -378,35 +379,35 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
setValueFromOpcode(opcode, volume, Default::volumeRange); setValueFromOpcode(opcode, volume, Default::volumeRange);
break; break;
case_any_ccN("volume"): // also gain case_any_ccN("volume"): // also gain
processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume]); processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume], ModKey::createNXYZ(ModId::Volume, id));
break; break;
case hash("amplitude"): case hash("amplitude"):
if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) if (auto value = readOpcode(opcode.value, Default::amplitudeRange))
amplitude = normalizePercents(*value); amplitude = normalizePercents(*value);
break; break;
case_any_ccN("amplitude"): case_any_ccN("amplitude"):
processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude]); processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude], ModKey::createNXYZ(ModId::Amplitude, id));
break; break;
case hash("pan"): case hash("pan"):
if (auto value = readOpcode(opcode.value, Default::panRange)) if (auto value = readOpcode(opcode.value, Default::panRange))
pan = normalizePercents(*value); pan = normalizePercents(*value);
break; break;
case_any_ccN("pan"): case_any_ccN("pan"):
processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan]); processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan], ModKey::createNXYZ(ModId::Pan, id));
break; break;
case hash("position"): case hash("position"):
if (auto value = readOpcode(opcode.value, Default::positionRange)) if (auto value = readOpcode(opcode.value, Default::positionRange))
position = normalizePercents(*value); position = normalizePercents(*value);
break; break;
case_any_ccN("position"): case_any_ccN("position"):
processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position]); processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position], ModKey::createNXYZ(ModId::Position, id));
break; break;
case hash("width"): case hash("width"):
if (auto value = readOpcode(opcode.value, Default::widthRange)) if (auto value = readOpcode(opcode.value, Default::widthRange))
width = normalizePercents(*value); width = normalizePercents(*value);
break; break;
case_any_ccN("width"): case_any_ccN("width"):
processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width]); processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width], ModKey::createNXYZ(ModId::Width, id));
break; break;
case hash("amp_keycenter"): case hash("amp_keycenter"):
setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); setValueFromOpcode(opcode, ampKeycenter, Default::keyRange);
@ -770,7 +771,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
setValueFromOpcode(opcode, tune, Default::tuneRange); setValueFromOpcode(opcode, tune, Default::tuneRange);
break; break;
case_any_ccN("pitch"): // also tune case_any_ccN("pitch"): // also tune
processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch]); processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch], ModKey::createNXYZ(ModId::Pitch, id));
break; break;
case hash("bend_up"): // also bendup case hash("bend_up"): // also bendup
setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); setValueFromOpcode(opcode, bendUp, Default::bendBoundRange);
@ -924,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
return true; 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, CCMap<Modifier> *ccMap, const ModKey& target)
{ {
if (!opcode.isAnyCcN()) if (!opcode.isAnyCcN())
return false; return false;
@ -933,6 +934,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range<float> range, CCM
if (ccNumber >= config::numCCs) if (ccNumber >= config::numCCs)
return false; return false;
// TODO obsolete after implementing mod matrix
if (ccMap) { if (ccMap) {
Modifier& modifier = (*ccMap)[ccNumber]; Modifier& modifier = (*ccMap)[ccNumber];
switch (opcode.category) { switch (opcode.category) {
@ -955,7 +957,53 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range<float> range, CCM
assert(false); assert(false);
break; break;
} }
} }
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.first.id() == ModId::Controller &&
x.first.parameters().cc == ccNumber &&
x.second == target;
});
Connection *conn;
if (it != connections.end())
conn = &*it;
else {
connections.emplace_back();
conn = &connections.back();
conn->first = ModKey::createCC(ccNumber, 0, 0, 0, 0);
conn->second = target;
}
//
ModKey::Parameters p = conn->first.parameters();
switch (opcode.category) {
case kOpcodeOnCcN:
setValueFromOpcode(opcode, p.value, range);
break;
case kOpcodeCurveCcN:
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, p.step, stepCCRange);
}
break;
case kOpcodeSmoothCcN:
setValueFromOpcode(opcode, p.smooth, Default::smoothCCRange);
break;
default:
assert(false);
break;
}
conn->first = ModKey(ModId::Controller, {}, p);
}
return true; return true;
} }

View file

@ -18,6 +18,7 @@
#include "FileId.h" #include "FileId.h"
#include "NumericId.h" #include "NumericId.h"
#include "Modifiers.h" #include "Modifiers.h"
#include "modulations/ModKey.h"
#include "absl/types/optional.h" #include "absl/types/optional.h"
#include <bitset> #include <bitset>
#include <string> #include <string>
@ -241,10 +242,11 @@ struct Region {
* @param opcode * @param opcode
* @param range * @param range
* @param ccMap * @param ccMap
* @param target
* @return true if the opcode was properly read and stored. * @return true if the opcode was properly read and stored.
* @return false * @return false
*/ */
bool processGenericCc(const Opcode& opcode, Range<float> range, CCMap<Modifier> *ccMap); bool processGenericCc(const Opcode& opcode, Range<float> range, CCMap<Modifier> *ccMap, const ModKey& target);
void offsetAllKeys(int offset) noexcept; void offsetAllKeys(int offset) noexcept;
@ -374,6 +376,10 @@ struct Region {
bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnCC { false }; // whether the region triggers on CC events or note events
bool triggerOnNote { true }; bool triggerOnNote { true };
// Modulation matrix connections
typedef std::pair<ModKey, ModKey> Connection;
std::vector<Connection> connections;
// Parent // Parent
RegionSet* parent { nullptr }; RegionSet* parent { nullptr };
private: private:

View file

@ -14,6 +14,7 @@
#include "Wavetables.h" #include "Wavetables.h"
#include "Curve.h" #include "Curve.h"
#include "Tuning.h" #include "Tuning.h"
#include "modulations/ModMatrix.h"
#include "absl/types/optional.h" #include "absl/types/optional.h"
namespace sfz namespace sfz
@ -33,18 +34,21 @@ struct Resources
WavetablePool wavePool; WavetablePool wavePool;
Tuning tuning; Tuning tuning;
absl::optional<StretchTuning> stretch; absl::optional<StretchTuning> stretch;
ModMatrix modMatrix;
void setSampleRate(float samplerate) void setSampleRate(float samplerate)
{ {
midiState.setSampleRate(samplerate); midiState.setSampleRate(samplerate);
filterPool.setSampleRate(samplerate); filterPool.setSampleRate(samplerate);
eqPool.setSampleRate(samplerate); eqPool.setSampleRate(samplerate);
modMatrix.setSampleRate(samplerate);
} }
void setSamplesPerBlock(int samplesPerBlock) void setSamplesPerBlock(int samplesPerBlock)
{ {
bufferPool.setBufferSize(samplesPerBlock); bufferPool.setBufferSize(samplesPerBlock);
midiState.setSamplesPerBlock(samplesPerBlock); midiState.setSamplesPerBlock(samplesPerBlock);
modMatrix.setSamplesPerBlock(samplesPerBlock);
} }
void clear() void clear()
@ -54,6 +58,7 @@ struct Resources
wavePool.clearFileWaves(); wavePool.clearFileWaves();
logger.clear(); logger.clear();
midiState.reset(); midiState.reset();
modMatrix.clear();
} }
}; };
} }

View file

@ -12,6 +12,10 @@
#include "ModifierHelpers.h" #include "ModifierHelpers.h"
#include "ScopedFTZ.h" #include "ScopedFTZ.h"
#include "StringViewHelpers.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 "pugixml.hpp"
#include "absl/algorithm/container.h" #include "absl/algorithm/container.h"
#include "absl/memory/memory.h" #include "absl/memory/memory.h"
@ -35,6 +39,9 @@ sfz::Synth::Synth(int numVoices)
effectFactory.registerStandardEffectTypes(); effectFactory.registerStandardEffectTypes();
effectBuses.reserve(5); // sufficient room for main and fx1-4 effectBuses.reserve(5); // sufficient room for main and fx1-4
resetVoices(numVoices); resetVoices(numVoices);
// modulation sources
genController.reset(new ControllerSource(resources));
} }
sfz::Synth::~Synth() sfz::Synth::~Synth()
@ -570,6 +577,8 @@ void sfz::Synth::finalizeSfzLoad()
settingsPerVoice.maxModifiers = maxModifiers; settingsPerVoice.maxModifiers = maxModifiers;
applySettingsPerVoice(); applySettingsPerVoice();
setupModMatrix();
} }
bool sfz::Synth::loadScalaFile(const fs::path& path) bool sfz::Synth::loadScalaFile(const fs::path& path)
@ -717,6 +726,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
return; return;
} }
ModMatrix& mm = resources.modMatrix;
activeVoices = 0; activeVoices = 0;
{ // Main render block { // Main render block
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
@ -724,6 +735,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
tempMixSpan->fill(0.0f); tempMixSpan->fill(0.0f);
resources.filePool.cleanupPromises(); resources.filePool.cleanupPromises();
mm.beginCycle(numFrames);
// Ramp out whatever is in the buffer at this point; should only be killed voice data // Ramp out whatever is in the buffer at this point; should only be killed voice data
linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames)); linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames));
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
@ -736,6 +749,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
if (voice->isFree()) if (voice->isFree())
continue; continue;
mm.beginVoice(voice->getId());
activeVoices++; activeVoices++;
renderVoiceToOutputs(*voice, *tempSpan); renderVoiceToOutputs(*voice, *tempSpan);
callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.data += voice->getLastDataDuration();
@ -1335,6 +1350,52 @@ void sfz::Synth::applySettingsPerVoice()
} }
} }
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.first.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.first, *gen);
ModMatrix::TargetId target = mm.registerTarget(conn.second);
ASSERT(source);
if (!source) {
DBG("[sfizz] Failed to register modulation source");
continue;
}
ASSERT(target);
if (!source) {
DBG("[sfizz] Failed to register modulation target");
continue;
}
if (!mm.connect(source, target)) {
DBG("[sfizz] Failed to connect modulation source and target");
ASSERTFALSE;
}
}
}
mm.init();
}
void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
{ {
const std::lock_guard<SpinMutex> disableCallback { callbackGuard }; const std::lock_guard<SpinMutex> disableCallback { callbackGuard };

View file

@ -26,6 +26,8 @@
#include <vector> #include <vector>
namespace sfz { namespace sfz {
class ControllerSource;
/** /**
* @brief This class is the core of the sfizz library. In C++ it is the main point * @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 * of entry and in C the interface basically maps the functions of the class into
@ -677,6 +679,11 @@ private:
*/ */
void applySettingsPerVoice(); void applySettingsPerVoice();
/**
* @brief Establish all connections of the modulation matrix.
*/
void setupModMatrix();
/** /**
* @brief Render the voice to its designated outputs and effect busses. * @brief Render the voice to its designated outputs and effect busses.
* *
@ -758,6 +765,9 @@ private:
int noteOffset { 0 }; int noteOffset { 0 };
int octaveOffset { 0 }; int octaveOffset { 0 };
// Modulation source generators
std::unique_ptr<ControllerSource> genController;
// Settings per voice // Settings per voice
struct SettingsPerVoice { struct SettingsPerVoice {
size_t maxFilters { 0 }; size_t maxFilters { 0 };

View file

@ -12,6 +12,9 @@
#include "SIMDHelpers.h" #include "SIMDHelpers.h"
#include "Panning.h" #include "Panning.h"
#include "SfzHelpers.h" #include "SfzHelpers.h"
#include "modulations/ModId.h"
#include "modulations/ModKey.h"
#include "modulations/ModMatrix.h"
#include "Interpolators.h" #include "Interpolators.h"
#include "absl/algorithm/container.h" #include "absl/algorithm/container.h"
@ -164,6 +167,8 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
smoother.setSmoothing(mod.data.smooth, sampleRate); smoother.setSmoothing(mod.data.smooth, sampleRate);
}); });
} }
resources.modMatrix.initVoice(id);
} }
int sfz::Voice::getCurrentSampleQuality() const noexcept int sfz::Voice::getCurrentSampleQuality() const noexcept
@ -374,30 +379,24 @@ void sfz::Voice::amplitudeEnvelope(absl::Span<float> modulationSpan) noexcept
{ {
const auto numSamples = modulationSpan.size(); const auto numSamples = modulationSpan.size();
auto tempSpan = resources.bufferPool.getBuffer(numSamples); ModMatrix& mm = resources.modMatrix;
if (!tempSpan) const ModKey volumeKey = ModKey::createNXYZ(ModId::Volume, region->getId());
return; const ModKey amplitudeKey = ModKey::createNXYZ(ModId::Amplitude, region->getId());
// AmpEG envelope // AmpEG envelope
egEnvelope.getBlock(modulationSpan); egEnvelope.getBlock(modulationSpan);
// Amplitude envelope // Amplitude envelope
applyGain1<float>(baseGain, modulationSpan); if (float* mod = mm.getModulationByKey(amplitudeKey)) {
forEachWithSmoother(Mod::amplitude, [&](const CCData<Modifier>& mod, Smoother& smoother) { for (size_t i = 0; i < numSamples; ++i)
linearModifier(resources, *tempSpan, mod, normalizePercents<float>); modulationSpan[i] *= normalizePercents(mod[i]);
smoother.process(*tempSpan, *tempSpan); }
applyGain<float>(*tempSpan, modulationSpan);
});
// Volume envelope // Volume envelope
applyGain1<float>(db2mag(baseVolumedB), modulationSpan); if (float* mod = mm.getModulationByKey(volumeKey)) {
forEachWithSmoother(Mod::volume, [&](const CCData<Modifier>& mod, Smoother& smoother) { for (size_t i = 0; i < numSamples; ++i)
multiplicativeModifier(resources, *tempSpan, mod, [](float x) { modulationSpan[i] *= db2mag(mod[i]);
return db2mag(x); }
});
smoother.process(*tempSpan, *tempSpan);
applyGain<float>(*tempSpan, modulationSpan);
});
// Smooth the gain transitions // Smooth the gain transitions
gainSmoother.process(modulationSpan, modulationSpan); gainSmoother.process(modulationSpan, modulationSpan);
@ -442,20 +441,21 @@ void sfz::Voice::panStageMono(AudioSpan<float> buffer) noexcept
const auto rightBuffer = buffer.getSpan(1); const auto rightBuffer = buffer.getSpan(1);
auto modulationSpan = resources.bufferPool.getBuffer(numSamples); auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
auto tempSpan = resources.bufferPool.getBuffer(numSamples); if (!modulationSpan)
if (!modulationSpan || !tempSpan)
return; return;
ModMatrix& mm = resources.modMatrix;
const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId());
// Prepare for stereo output // Prepare for stereo output
copy<float>(leftBuffer, rightBuffer); copy<float>(leftBuffer, rightBuffer);
// Apply panning // Apply panning
fill(*modulationSpan, region->pan); fill(*modulationSpan, region->pan);
forEachWithSmoother(Mod::pan, [&](const CCData<Modifier>& mod, Smoother& smoother) { if (float* mod = mm.getModulationByKey(panKey)) {
linearModifier(resources, *tempSpan, mod, normalizePercents<float>); for (size_t i = 0; i < numSamples; ++i)
smoother.process(*tempSpan, *tempSpan); (*modulationSpan)[i] += normalizePercents(mod[i]);
add<float>(*tempSpan, *modulationSpan); }
});
pan(*modulationSpan, leftBuffer, rightBuffer); pan(*modulationSpan, leftBuffer, rightBuffer);
} }
@ -467,34 +467,35 @@ void sfz::Voice::panStageStereo(AudioSpan<float> buffer) noexcept
const auto rightBuffer = buffer.getSpan(1); const auto rightBuffer = buffer.getSpan(1);
auto modulationSpan = resources.bufferPool.getBuffer(numSamples); auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
auto tempSpan = resources.bufferPool.getBuffer(numSamples); if (!modulationSpan)
if (!modulationSpan || !tempSpan)
return; 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 // Apply panning
fill(*modulationSpan, region->pan); fill(*modulationSpan, region->pan);
forEachWithSmoother(Mod::pan, [&](const CCData<Modifier>& mod, Smoother& smoother) { if (float* mod = mm.getModulationByKey(panKey)) {
linearModifier(resources, *tempSpan, mod, normalizePercents<float>); for (size_t i = 0; i < numSamples; ++i)
smoother.process(*tempSpan, *tempSpan); (*modulationSpan)[i] += normalizePercents(mod[i]);
add<float>(*tempSpan, *modulationSpan); }
});
pan(*modulationSpan, leftBuffer, rightBuffer); pan(*modulationSpan, leftBuffer, rightBuffer);
// Apply the width/position process // Apply the width/position process
fill(*modulationSpan, region->width); fill(*modulationSpan, region->width);
forEachWithSmoother(Mod::width, [&](const CCData<Modifier>& mod, Smoother& smoother) { if (float* mod = mm.getModulationByKey(widthKey)) {
linearModifier(resources, *tempSpan, mod, normalizePercents<float>); for (size_t i = 0; i < numSamples; ++i)
smoother.process(*tempSpan, *tempSpan); (*modulationSpan)[i] += normalizePercents(mod[i]);
add<float>(*tempSpan, *modulationSpan); }
});
width(*modulationSpan, leftBuffer, rightBuffer); width(*modulationSpan, leftBuffer, rightBuffer);
fill(*modulationSpan, region->position); fill(*modulationSpan, region->position);
forEachWithSmoother(Mod::position, [&](const CCData<Modifier>& mod, Smoother& smoother) { if (float* mod = mm.getModulationByKey(positionKey)) {
linearModifier(resources, *tempSpan, mod, normalizePercents<float>); for (size_t i = 0; i < numSamples; ++i)
smoother.process(*tempSpan, *tempSpan); (*modulationSpan)[i] += normalizePercents(mod[i]);
add<float>(*tempSpan, *modulationSpan); }
});
pan(*modulationSpan, leftBuffer, rightBuffer); pan(*modulationSpan, leftBuffer, rightBuffer);
} }
@ -906,13 +907,13 @@ void sfz::Voice::pitchEnvelope(absl::Span<float> pitchSpan) noexcept
bendSmoother.process(*bends, *bends); bendSmoother.process(*bends, *bends);
applyGain<float>(*bends, pitchSpan); applyGain<float>(*bends, pitchSpan);
forEachWithSmoother(Mod::pitch, [&](const CCData<Modifier>& mod, Smoother& smoother) { ModMatrix& mm = resources.modMatrix;
multiplicativeModifier(resources, *bends, mod, [](float x) { const ModKey pitchKey = ModKey::createNXYZ(ModId::Pitch, region->getId());
return centsFactor(x);
}); if (float* mod = mm.getModulationByKey(pitchKey)) {
smoother.process(*bends, *bends); for (size_t i = 0; i < numFrames; ++i)
applyGain<float>(*bends, pitchSpan); pitchSpan[i] *= centsFactor(mod[i]);
}); }
} }
void sfz::Voice::resetSmoothers() noexcept void sfz::Voice::resetSmoothers() noexcept

View file

@ -0,0 +1,52 @@
// 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) = 0;
/**
* @brief Set the maximum block size
*/
virtual void setSamplesPerBlock(unsigned count) = 0;
/**
* @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;
};
} // 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,93 @@
// 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 ModKey::createCC(uint8_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_.value, "}");
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_ &&
!std::memcmp(&parameters(), &other.parameters(), sizeof(ModKey::Parameters));
}
bool sfz::ModKey::operator!=(const ModKey &other) const noexcept
{
return !this->operator==(other);
}

View file

@ -0,0 +1,68 @@
// 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>
#include <cstring>
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(uint8_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() { std::memset(this, 0, sizeof(*this)); }
union {
//! Parameters if this key identifies a CC source
struct { uint8_t cc, 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; };
};
};
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 <cstddef>
namespace sfz { class ModKey; }
namespace std {
template <class> struct hash;
template <> struct hash<sfz::ModKey> {
size_t operator()(const sfz::ModKey &key) const;
};
}

View file

@ -0,0 +1,319 @@
// 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 <vector>
#include <algorithm>
namespace sfz {
struct ModMatrix::Impl {
double sampleRate_ {};
uint32_t samplesPerBlock_ {};
uint32_t numFrames_ {};
NumericId<Voice> voiceId_ {};
struct Source {
ModKey key;
ModGenerator* gen {};
bool bufferReady {};
Buffer<float> buffer;
};
struct ConnectionData {
// nothing
};
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_;
Buffer<float> temp_;
};
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);
impl.temp_.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)
{
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];
return true;
}
void ModMatrix::init()
{
Impl& impl = *impl_;
for (Impl::Source &source : impl.sources_) {
int flags = source.key.flags();
if (flags & kModIsPerCycle)
source.gen->init(source.key, {});
}
}
void ModMatrix::initVoice(NumericId<Voice> voiceId)
{
Impl& impl = *impl_;
for (Impl::Source &source : impl.sources_) {
int flags = source.key.flags();
if (flags & kModIsPerVoice)
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::beginVoice(NumericId<Voice> voiceId)
{
Impl& impl = *impl_;
impl.voiceId_ = voiceId;
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;
}
}
float* ModMatrix::getModulation(TargetId targetId)
{
if (!validTarget(targetId))
return nullptr;
Impl& impl = *impl_;
const uint32_t targetIndex = targetId.number();
Impl::Target &target = impl.targets_[targetIndex];
const int flags = target.key.flags();
const uint32_t numFrames = impl.numFrames_;
absl::Span<float> buffer(target.buffer.data(), numFrames);
// 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;
if (flags & kModIsMultiplicative)
sfz::fill(buffer, 1.0f);
else if (flags & kModIsPercentMultiplicative)
sfz::fill(buffer, 100.0f);
else {
ASSERT(flags & kModIsAdditive);
sfz::fill(buffer, 0.0f);
}
auto sourcesPos = target.connectedSources.begin();
auto sourcesEnd = target.connectedSources.end();
// generate the first source in buffer
if (sourcesPos != sourcesEnd) {
Impl::Source &source = impl.sources_[sourcesPos->first];
source.gen->generate(source.key, impl.voiceId_, buffer);
++sourcesPos;
}
// generate next sources in temporary buffer
// then add or multiply, depending on target flags
absl::Span<float> temp(impl.temp_.data(), numFrames);
while (sourcesPos != sourcesEnd) {
Impl::Source &source = impl.sources_[sourcesPos->first];
source.gen->generate(source.key, impl.voiceId_, temp);
if (flags & kModIsMultiplicative) {
for (uint32_t i = 0; i < numFrames; ++i)
buffer[i] *= temp[i];
}
else if (flags & kModIsPercentMultiplicative) {
for (uint32_t i = 0; i < numFrames; ++i)
buffer[i] *= 0.01f * temp[i];
}
else {
ASSERT(flags & kModIsAdditive);
for (uint32_t i = 0; i < numFrames; ++i)
buffer[i] += temp[i];
}
++sourcesPos;
}
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();
}
} // namespace sfz

View file

@ -0,0 +1,160 @@
// 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 <memory>
#include <cstdint>
namespace sfz {
class ModKey;
class ModGenerator;
class Voice;
/**
* @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
* @return true if the connection was successfully made, otherwise false
*/
bool connect(SourceId sourceId, TargetId targetId);
/**
* @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);
/**
* @brief Start modulation processing for the entire cycle.
* This clears all the buffers.
*
* @param numFrames
*/
void beginCycle(unsigned numFrames);
/**
* @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
*/
void beginVoice(NumericId<Voice> voiceId);
/**
* @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;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace sfz

View file

@ -0,0 +1,97 @@
// 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;
#pragma message("TODO: implement CC shortcut")
bool canShortcut = false;
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

@ -36,6 +36,7 @@ set(SFIZZ_TEST_SOURCES
SwapAndPopT.cpp SwapAndPopT.cpp
TuningT.cpp TuningT.cpp
ConcurrencyT.cpp ConcurrencyT.cpp
ModulationsT.cpp
) )
add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES})

76
tests/ModulationsT.cpp Normal file
View file

@ -0,0 +1,76 @@
// 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 "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(((flags & sfz::kModIsPerCycle) ^
(flags & sfz::kModIsPerVoice)) != 0);
};
static auto* checkSourceFlags = +[](int flags)
{
checkBasicFlags(flags);
// nothing else
};
static auto* checkTargetFlags = +[](int flags)
{
checkBasicFlags(flags);
REQUIRE(((flags & sfz::kModIsAdditive) ^
(flags & sfz::kModIsMultiplicative) ^
(flags & sfz::kModIsPercentMultiplicative)) != 0);
};
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());
});
}