From 675d7f3fc5ea234e78aa27bb65bdfbb4cba32ac6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 19:25:59 +0200 Subject: [PATCH] Add modulation matrix --- dpf.mk | 5 + src/CMakeLists.txt | 11 + src/sfizz/Region.cpp | 64 +++- src/sfizz/Region.h | 8 +- src/sfizz/Resources.h | 5 + src/sfizz/Synth.cpp | 61 ++++ src/sfizz/Synth.h | 10 + src/sfizz/Voice.cpp | 97 +++--- src/sfizz/modulations/ModGenerator.h | 52 +++ src/sfizz/modulations/ModId.cpp | 54 ++++ src/sfizz/modulations/ModId.h | 87 +++++ src/sfizz/modulations/ModKey.cpp | 93 ++++++ src/sfizz/modulations/ModKey.h | 68 ++++ src/sfizz/modulations/ModKeyHash.cpp | 34 ++ src/sfizz/modulations/ModKeyHash.h | 17 + src/sfizz/modulations/ModMatrix.cpp | 319 +++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 160 ++++++++++ src/sfizz/modulations/sources/Controller.cpp | 97 ++++++ src/sfizz/modulations/sources/Controller.h | 29 ++ tests/CMakeLists.txt | 1 + tests/ModulationsT.cpp | 76 +++++ 21 files changed, 1291 insertions(+), 57 deletions(-) create mode 100644 src/sfizz/modulations/ModGenerator.h create mode 100644 src/sfizz/modulations/ModId.cpp create mode 100644 src/sfizz/modulations/ModId.h create mode 100644 src/sfizz/modulations/ModKey.cpp create mode 100644 src/sfizz/modulations/ModKey.h create mode 100644 src/sfizz/modulations/ModKeyHash.cpp create mode 100644 src/sfizz/modulations/ModKeyHash.h create mode 100644 src/sfizz/modulations/ModMatrix.cpp create mode 100644 src/sfizz/modulations/ModMatrix.h create mode 100644 src/sfizz/modulations/sources/Controller.cpp create mode 100644 src/sfizz/modulations/sources/Controller.h create mode 100644 tests/ModulationsT.cpp diff --git a/dpf.mk b/dpf.mk index f4442beb..4060db8d 100644 --- a/dpf.mk +++ b/dpf.mk @@ -60,6 +60,11 @@ SFIZZ_SOURCES = \ src/sfizz/Curve.cpp \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ + src/sfizz/modulations/ModId.cpp \ + src/sfizz/modulations/ModKey.cpp \ + src/sfizz/modulations/ModKeyHash.cpp \ + src/sfizz/modulations/ModMatrix.cpp \ + src/sfizz/modulations/sources/Controller.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c694239..f0343c5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 @@ -128,6 +134,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 diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0a19625c..c8775c0b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -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, &modifiers[Mod::volume], 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, &modifiers[Mod::amplitude], 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, &modifiers[Mod::pan], 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, &modifiers[Mod::position], 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, &modifiers[Mod::width], 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, &modifiers[Mod::pitch], 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 range, CCMap *ccMap) +bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target) { if (!opcode.isAnyCcN()) return false; @@ -933,6 +934,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM if (ccNumber >= config::numCCs) return false; + // TODO obsolete after implementing mod matrix if (ccMap) { Modifier& modifier = (*ccMap)[ccNumber]; switch (opcode.category) { @@ -955,7 +957,53 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM assert(false); 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 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; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 772ee399..da044fd9 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -18,6 +18,7 @@ #include "FileId.h" #include "NumericId.h" #include "Modifiers.h" +#include "modulations/ModKey.h" #include "absl/types/optional.h" #include #include @@ -241,10 +242,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 range, CCMap *ccMap); + bool processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target); 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 triggerOnNote { true }; + // Modulation matrix connections + typedef std::pair Connection; + std::vector connections; + // Parent RegionSet* parent { nullptr }; private: diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 0c28427e..645482cd 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -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 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(); } }; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 47fc507d..7bae9797 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -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() @@ -570,6 +577,8 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxModifiers = maxModifiers; applySettingsPerVoice(); + + setupModMatrix(); } bool sfz::Synth::loadScalaFile(const fs::path& path) @@ -717,6 +726,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; } + ModMatrix& mm = resources.modMatrix; + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; @@ -724,6 +735,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); + mm.beginCycle(numFrames); + // Ramp out whatever is in the buffer at this point; should only be killed voice data linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { @@ -736,6 +749,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; + mm.beginVoice(voice->getId()); + activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); 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 { const std::lock_guard disableCallback { callbackGuard }; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 13b1f9ba..5c41078d 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -26,6 +26,8 @@ #include 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,6 +765,9 @@ private: int noteOffset { 0 }; int octaveOffset { 0 }; + // Modulation source generators + std::unique_ptr genController; + // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 910fed95..a9ced8b5 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -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" @@ -164,6 +167,8 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, smoother.setSmoothing(mod.data.smooth, sampleRate); }); } + + resources.modMatrix.initVoice(id); } int sfz::Voice::getCurrentSampleQuality() const noexcept @@ -374,30 +379,24 @@ void sfz::Voice::amplitudeEnvelope(absl::Span 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(baseGain, modulationSpan); - forEachWithSmoother(Mod::amplitude, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - applyGain(*tempSpan, modulationSpan); - }); + if (float* mod = mm.getModulationByKey(amplitudeKey)) { + for (size_t i = 0; i < numSamples; ++i) + modulationSpan[i] *= normalizePercents(mod[i]); + } // Volume envelope - applyGain1(db2mag(baseVolumedB), modulationSpan); - forEachWithSmoother(Mod::volume, [&](const CCData& mod, Smoother& smoother) { - multiplicativeModifier(resources, *tempSpan, mod, [](float x) { - return db2mag(x); - }); - smoother.process(*tempSpan, *tempSpan); - applyGain(*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 +441,21 @@ void sfz::Voice::panStageMono(AudioSpan 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(leftBuffer, rightBuffer); // Apply panning fill(*modulationSpan, region->pan); - forEachWithSmoother(Mod::pan, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*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 +467,35 @@ void sfz::Voice::panStageStereo(AudioSpan 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& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*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& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*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& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*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); } @@ -906,13 +907,13 @@ void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept bendSmoother.process(*bends, *bends); applyGain(*bends, pitchSpan); - forEachWithSmoother(Mod::pitch, [&](const CCData& mod, Smoother& smoother) { - multiplicativeModifier(resources, *bends, mod, [](float x) { - return centsFactor(x); - }); - smoother.process(*bends, *bends); - applyGain(*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 diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h new file mode 100644 index 00000000..c146593d --- /dev/null +++ b/src/sfizz/modulations/ModGenerator.h @@ -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 +#include + +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 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 voiceNum, absl::Span buffer) = 0; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp new file mode 100644 index 00000000..a75c648c --- /dev/null +++ b/src/sfizz/modulations/ModId.cpp @@ -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(id) >= static_cast(ModId::_SourcesStart) && + static_cast(id) < static_cast(ModId::_SourcesEnd); +} + +bool ModIds::isTarget(ModId id) noexcept +{ + return static_cast(id) >= static_cast(ModId::_TargetsStart) && + static_cast(id) < static_cast(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 diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h new file mode 100644 index 00000000..8f18679a --- /dev/null +++ b/src/sfizz/modulations/ModId.h @@ -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 inline void forEachSourceId(F&& f) +{ + for (int i = static_cast(ModId::_SourcesStart); + i < static_cast(ModId::_SourcesEnd); ++i) + f(static_cast(i)); +} + +template inline void forEachTargetId(F&& f) +{ + for (int i = static_cast(ModId::_TargetsStart); + i < static_cast(ModId::_TargetsEnd); ++i) + f(static_cast(i)); +} + +} // namespace ModIds + +} // namespace sfz diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp new file mode 100644 index 00000000..3a865bd7 --- /dev/null +++ b/src/sfizz/modulations/ModKey.cpp @@ -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 +#include + +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, 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(¶meters(), &other.parameters(), sizeof(ModKey::Parameters)); +} + +bool sfz::ModKey::operator!=(const ModKey &other) const noexcept +{ + return !this->operator==(other); +} diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h new file mode 100644 index 00000000..e70c244d --- /dev/null +++ b/src/sfizz/modulations/ModKey.h @@ -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 +#include + +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 = {}, 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, 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() 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_; + //! List of values which identify the key uniquely, along with the hash and region + Parameters params_ {}; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/ModKeyHash.cpp b/src/sfizz/modulations/ModKeyHash.cpp new file mode 100644 index 00000000..8e274a45 --- /dev/null +++ b/src/sfizz/modulations/ModKeyHash.cpp @@ -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 + +size_t std::hash::operator()(const sfz::ModKey &key) const +{ + uint64_t k = hashNumber(static_cast(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; +} diff --git a/src/sfizz/modulations/ModKeyHash.h b/src/sfizz/modulations/ModKeyHash.h new file mode 100644 index 00000000..a5cdf4c6 --- /dev/null +++ b/src/sfizz/modulations/ModKeyHash.h @@ -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 + +namespace sfz { class ModKey; } + +namespace std { + template struct hash; + template <> struct hash { + size_t operator()(const sfz::ModKey &key) const; + }; +} diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp new file mode 100644 index 00000000..dd1bd4c3 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -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 +#include +#include + +namespace sfz { + +struct ModMatrix::Impl { + double sampleRate_ {}; + uint32_t samplesPerBlock_ {}; + + uint32_t numFrames_ {}; + NumericId voiceId_ {}; + + struct Source { + ModKey key; + ModGenerator* gen {}; + bool bufferReady {}; + Buffer buffer; + }; + + struct ConnectionData { + // nothing + }; + + struct Target { + ModKey key; + uint32_t region {}; + absl::flat_hash_map connectedSources; + bool bufferReady {}; + Buffer buffer; + }; + + absl::flat_hash_map sourceIndex_; + absl::flat_hash_map targetIndex_; + + std::vector sources_; + std::vector targets_; + + Buffer 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(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(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 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 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 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 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(id.number()) < impl_->targets_.size(); +} + +bool ModMatrix::validSource(SourceId id) const +{ + return static_cast(id.number()) < impl_->sources_.size(); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h new file mode 100644 index 00000000..34edb802 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.h @@ -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 +#include + +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 SourceId; + + //! Identifier of a modulation target + typedef NumericId 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 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 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_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp new file mode 100644 index 00000000..b60269f0 --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -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 + +namespace sfz { + +struct ControllerSource::Impl { + double sampleRate_ = config::defaultSampleRate; + Resources* res_ = nullptr; + absl::flat_hash_map 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 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 voiceId, absl::Span 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 diff --git a/src/sfizz/modulations/sources/Controller.h b/src/sfizz/modulations/sources/Controller.h new file mode 100644 index 00000000..1dfe26cd --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.h @@ -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 + +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 voiceId) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 21bc72d1..9b7155c5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,7 @@ set(SFIZZ_TEST_SOURCES SwapAndPopT.cpp TuningT.cpp ConcurrencyT.cpp + ModulationsT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp new file mode 100644 index 00000000..3db0a96a --- /dev/null +++ b/tests/ModulationsT.cpp @@ -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()); + }); +}