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..a1199157 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 @@ -67,7 +73,6 @@ set (SFIZZ_HEADERS sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h - sfizz/Modifiers.h sfizz/NumericId.h sfizz/OnePoleFilter.h sfizz/Oversampler.h @@ -128,6 +133,11 @@ set (SFIZZ_SOURCES sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp + sfizz/modulations/ModId.cpp + sfizz/modulations/ModKey.cpp + sfizz/modulations/ModKeyHash.cpp + sfizz/modulations/ModMatrix.cpp + sfizz/modulations/sources/Controller.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h index 609b16ff..e9b76361 100644 --- a/src/sfizz/ModifierHelpers.h +++ b/src/sfizz/ModifierHelpers.h @@ -8,8 +8,7 @@ #include "Range.h" #include "Defaults.h" -#include "Modifiers.h" -#include "Resources.h" +#include "SfzHelpers.h" #include "absl/types/span.h" namespace sfz { @@ -257,77 +256,4 @@ void pitchBendEnvelope(const EventVector& events, absl::Span envelope, F& multiplicativeEnvelope(events, envelope, std::forward(lambda)); } -/** - * @brief Builds a linear envelope, possibly quantized, based on the events fetched - * from a midi state and the modifier data. This is a helper function for recurrent - * code in the voice logic. - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -template -void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) -{ - const auto& events = resources.midiState.getCCEvents(ccData.cc); - const auto& curve = resources.curves.getCurve(ccData.data.curve); - if (ccData.data.step == 0.0f) { - linearEnvelope(events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }); - } else { - const float stepSize { lambda(ccData.data.step) }; - linearEnvelope( - events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }, - stepSize); - } -} - -/** - * @brief Builds a multiplicative envelope, possibly quantized, based on the events fetched - * from a midi state and the modifier data. This is a helper function for recurrent - * code in the voice logic. - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -template -void multiplicativeModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) -{ - const auto& events = resources.midiState.getCCEvents(ccData.cc); - const auto& curve = resources.curves.getCurve(ccData.data.curve); - if (ccData.data.step == 0.0f) { - multiplicativeEnvelope(events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }); - } else { - const float stepSize { lambda(ccData.data.step) }; - multiplicativeEnvelope( - events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }, - stepSize); - } -} - -/** - * @brief Alias for a simple linear modifier with no lambda - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -inline void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData) -{ - linearModifier(resources, span, ccData, [](float x) { return x; }); -} -} +} // namespace sfz diff --git a/src/sfizz/Modifiers.h b/src/sfizz/Modifiers.h deleted file mode 100644 index 4868d112..00000000 --- a/src/sfizz/Modifiers.h +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#pragma once -#include "Config.h" -#include -#include -#include -#include -#include - -namespace sfz { - -/** - * @brief Base modifier class - * - */ -struct Modifier { - float value { 0.0f }; - float step { 0.0f }; - uint8_t curve { 0 }; - uint8_t smooth { 0 }; - static_assert(config::maxCurves - 1 <= std::numeric_limits::max(), "The curve type in the Modifier struct cannot support the required number of curves"); -}; - -enum class Mod : size_t { - amplitude = 0, - pan, - width, - position, - pitch, - volume, - sentinel -}; - -/** - * @brief Vectors of elements indexed on modifiers with casting and iterators - * - * @tparam T - */ -template -class ModifierVector : public std::vector { -public: - T& operator[](sfz::Mod idx) { return this->std::vector::operator[](static_cast(idx)); } - const T& operator[](sfz::Mod idx) const { return this->std::vector::operator[](static_cast(idx)); } -}; - -/** - * @brief Array of elements indexed on modifiers with casting and iterators - * - * @tparam T - */ -template -class ModifierArray { -public: - using ContainerType = typename std::array; - using iterator = typename ContainerType::iterator; - using const_iterator = typename ContainerType::const_iterator; - ModifierArray() = default; - ModifierArray(T val) - { - std::fill(underlying.begin(), underlying.end(), val); - } - ModifierArray(std::array&& array) : underlying(array) {} - T& operator[](sfz::Mod idx) { return underlying.operator[](static_cast(idx)); } - const T& operator[](sfz::Mod idx) const { return underlying.operator[](static_cast(idx)); } - iterator begin() { return underlying.begin(); } - iterator end() { return underlying.end(); } - const_iterator begin() const { return underlying.begin(); } - const_iterator end() const { return underlying.end(); } -private: - ContainerType underlying {}; -}; - -/** - * @brief Helper for iterating over all possible modifiers. - * Should fail at compile time if you update the modifiers but not this. - * - */ -static const ModifierArray allModifiers {{ - Mod::amplitude, - Mod::pan, - Mod::width, - Mod::position, - Mod::pitch, - Mod::volume -}}; - -} diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index 87b25666..4d15f844 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -13,7 +13,7 @@ #include "absl/types/optional.h" #include "absl/meta/type_traits.h" #include "absl/strings/ascii.h" -#include +#include "absl/strings/string_view.h" #include #include #include diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0a19625c..25d38f40 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, ModKey::createNXYZ(ModId::Volume, id)); break; case hash("amplitude"): if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) amplitude = normalizePercents(*value); break; case_any_ccN("amplitude"): - processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude]); + processGenericCc(opcode, Default::amplitudeRange, ModKey::createNXYZ(ModId::Amplitude, id)); break; case hash("pan"): if (auto value = readOpcode(opcode.value, Default::panRange)) pan = normalizePercents(*value); break; case_any_ccN("pan"): - processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan]); + processGenericCc(opcode, Default::panCCRange, ModKey::createNXYZ(ModId::Pan, id)); break; case hash("position"): if (auto value = readOpcode(opcode.value, Default::positionRange)) position = normalizePercents(*value); break; case_any_ccN("position"): - processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position]); + processGenericCc(opcode, Default::positionCCRange, ModKey::createNXYZ(ModId::Position, id)); break; case hash("width"): if (auto value = readOpcode(opcode.value, Default::widthRange)) width = normalizePercents(*value); break; case_any_ccN("width"): - processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width]); + processGenericCc(opcode, Default::widthCCRange, ModKey::createNXYZ(ModId::Width, id)); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -770,7 +771,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, tune, Default::tuneRange); break; case_any_ccN("pitch"): // also tune - processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch]); + processGenericCc(opcode, Default::tuneCCRange, ModKey::createNXYZ(ModId::Pitch, id)); break; case hash("bend_up"): // also bendup setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -924,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } -bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap) +bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, const ModKey& target) { if (!opcode.isAnyCcN()) return false; @@ -933,29 +934,51 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM if (ccNumber >= config::numCCs) return false; - if (ccMap) { - Modifier& modifier = (*ccMap)[ccNumber]; + if (target) { + // search an existing connection of same CC number and target + // if it exists, modify, otherwise create + auto it = std::find_if(connections.begin(), connections.end(), + [ccNumber, &target](const Connection& x) -> bool + { + return x.source.id() == ModId::Controller && + x.source.parameters().cc == ccNumber && + x.target == target; + }); + + Connection *conn; + if (it != connections.end()) + conn = &*it; + else { + connections.emplace_back(); + conn = &connections.back(); + conn->source = ModKey::createCC(ccNumber, 0, 0, 0, 0); + conn->target = target; + } + + // + ModKey::Parameters p = conn->source.parameters(); switch (opcode.category) { case kOpcodeOnCcN: - setValueFromOpcode(opcode, modifier.value, range); + setValueFromOpcode(opcode, p.value, range); break; case kOpcodeCurveCcN: - setValueFromOpcode(opcode, modifier.curve, Default::curveCCRange); + setValueFromOpcode(opcode, p.curve, Default::curveCCRange); break; case kOpcodeStepCcN: { const Range stepCCRange { 0.0f, std::max(std::abs(range.getStart()), std::abs(range.getEnd())) }; - setValueFromOpcode(opcode, modifier.step, stepCCRange); + setValueFromOpcode(opcode, p.step, stepCCRange); } break; case kOpcodeSmoothCcN: - setValueFromOpcode(opcode, modifier.smooth, Default::smoothCCRange); + setValueFromOpcode(opcode, p.smooth, Default::smoothCCRange); break; default: assert(false); break; } - } + conn->source = ModKey(ModId::Controller, {}, p); + } return true; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 772ee399..f226a331 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -17,8 +17,9 @@ #include "MidiState.h" #include "FileId.h" #include "NumericId.h" -#include "Modifiers.h" +#include "modulations/ModKey.h" #include "absl/types/optional.h" +#include "absl/strings/string_view.h" #include #include #include @@ -240,11 +241,11 @@ struct Region { * * @param opcode * @param range - * @param ccMap + * @param target * @return true if the opcode was properly read and stored. * @return false */ - bool processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap); + bool processGenericCc(const Opcode& opcode, Range range, const ModKey& target); void offsetAllKeys(int offset) noexcept; @@ -368,12 +369,17 @@ struct Region { // Effects std::vector gainToEffect; - // Modifiers - ModifierArray> modifiers; - bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; + // Modulation matrix connections + struct Connection { + ModKey source; + ModKey target; + float sourceDepth = 1.0f; + }; + std::vector 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..71f9f500 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() @@ -445,7 +452,6 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -556,8 +562,6 @@ void sfz::Synth::finalizeSfzLoad() region->registerTempo(2.0f); maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); - for (const auto& mod : allModifiers) - maxModifiers[mod] = max(maxModifiers[mod], region->modifiers[mod].size()); ++currentRegionIndex; } @@ -567,9 +571,10 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; - settingsPerVoice.maxModifiers = maxModifiers; applySettingsPerVoice(); + + setupModMatrix(); } bool sfz::Synth::loadScalaFile(const fs::path& path) @@ -717,6 +722,9 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; } + ModMatrix& mm = resources.modMatrix; + mm.beginCycle(numFrames); + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; @@ -736,6 +744,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; + mm.beginVoice(voice->getId(), voice->getRegion()->getId()); + activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); callbackBreakdown.data += voice->getLastDataDuration(); @@ -743,6 +753,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.filters += voice->getLastFilterDuration(); callbackBreakdown.panning += voice->getLastPanningDuration(); + mm.endVoice(); + if (voice->toBeCleanedUp()) voice->reset(); } @@ -770,6 +782,9 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept // Apply the master volume buffer.applyGain(db2mag(volume)); + // Perform any remaining modulators + mm.endCycle(); + { // Clear events and advance midi time ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.advanceTime(buffer.getNumFrames()); @@ -1331,10 +1346,55 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); - voice->prepareSmoothers(settingsPerVoice.maxModifiers); } } +void sfz::Synth::setupModMatrix() +{ + ModMatrix& mm = resources.modMatrix; + + for (const RegionPtr& region : regions) { + for (const Region::Connection& conn : region->connections) { + ModGenerator* gen = nullptr; + + switch (conn.source.id()) { + case ModId::Controller: + gen = genController.get(); + break; + default: + DBG("[sfizz] Have unknown type of source generator"); + break; + } + + ASSERT(gen); + if (!gen) + continue; + + ModMatrix::SourceId source = mm.registerSource(conn.source, *gen); + ModMatrix::TargetId target = mm.registerTarget(conn.target); + + ASSERT(source); + if (!source) { + DBG("[sfizz] Failed to register modulation source"); + continue; + } + + ASSERT(target); + if (!target) { + DBG("[sfizz] Failed to register modulation target"); + continue; + } + + if (!mm.connect(source, target, conn.sourceDepth)) { + DBG("[sfizz] Failed to connect modulation source and target"); + ASSERTFALSE; + } + } + } + + mm.init(); +} + void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept { const std::lock_guard disableCallback { callbackGuard }; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 13b1f9ba..8133b676 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -18,14 +18,16 @@ #include "parser/Parser.h" #include "VoiceStealing.h" #include "utility/SpinMutex.h" -#include "absl/types/span.h" +#include #include +#include #include #include -#include #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,11 +765,13 @@ private: int noteOffset { 0 }; int octaveOffset { 0 }; + // Modulation source generators + std::unique_ptr genController; + // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 910fed95..97fa044d 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" @@ -138,32 +141,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); - for (auto& modId : allModifiers) { - ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size()); - forEachWithSmoother(modId, [modId, this](const CCData& mod, Smoother& smoother) { - const auto ccValue = resources.midiState.getCCValue(mod.cc); - const auto& curve = resources.curves.getCurve(mod.data.curve); - const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value; - switch (modId) { - case Mod::volume: - smoother.reset(db2mag(finalValue)); - break; - case Mod::pitch: - smoother.reset(centsFactor(finalValue)); - break; - case Mod::amplitude: - case Mod::pan: - case Mod::width: - case Mod::position: - smoother.reset(normalizePercents(finalValue)); - break; - default: - smoother.reset(finalValue); - break; - } - smoother.setSmoothing(mod.data.smooth, sampleRate); - }); - } + resources.modMatrix.initVoice(id, region->getId()); } int sfz::Voice::getCurrentSampleQuality() const noexcept @@ -374,30 +352,24 @@ void sfz::Voice::amplitudeEnvelope(absl::Span 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 +414,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 +440,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); } @@ -881,12 +855,6 @@ void sfz::Voice::switchState(State s) } } -void sfz::Voice::prepareSmoothers(const ModifierArray& numModifiers) -{ - for (auto& mod : allModifiers) - modifierSmoothers[mod].resize(numModifiers[mod]); -} - void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept { const auto numFrames = pitchSpan.size(); @@ -906,32 +874,17 @@ 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 { - for (auto& mod : allModifiers) { - const auto resetValue = [mod] { - switch (mod) { - case Mod::volume: // fallthrough - case Mod::pitch: - return 1.0f; - default: - return 0.0f; - } - }(); - - for (auto& smoother : modifierSmoothers[mod]) { - smoother.reset(resetValue); - } - } bendSmoother.reset(1.0f); gainSmoother.reset(0.0f); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index f567c5e6..52a7f0c8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -302,8 +302,6 @@ public: Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } - void prepareSmoothers(const ModifierArray& numModifiers); - private: /** * @brief Fill a span with data from a file source. This is the first step @@ -390,27 +388,6 @@ private: */ void removeVoiceFromRing() noexcept; - /** - * @brief Helper function to iterate jointly on modifiers and smoothers - * for a given modulation target of type sfz::Mod - * - * @tparam F - * @param modId - * @param lambda - */ - template - void forEachWithSmoother(sfz::Mod modId, F&& lambda) - { - size_t count = region->modifiers[modId].size(); - ASSERT(modifierSmoothers[modId].size() >= count); - auto mod = region->modifiers[modId].begin(); - auto smoother = modifierSmoothers[modId].begin(); - for (size_t i = 0; i < count; ++i) { - lambda(*mod, *smoother); - incrementAll(mod, smoother); - } - } - /** * @brief Initialize frequency and gain coefficients for the oscillators. */ @@ -479,7 +456,6 @@ private: fast_real_distribution uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds }; fast_gaussian_generator gaussianNoiseDist { 0.0f, config::noiseVariance }; - ModifierArray> modifierSmoothers; Smoother gainSmoother; Smoother bendSmoother; Smoother xfadeSmoother; diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h new file mode 100644 index 00000000..2229ec15 --- /dev/null +++ b/src/sfizz/modulations/ModGenerator.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../NumericId.h" +#include +#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) { (void)sampleRate; } + + /** + * @brief Set the maximum block size + */ + virtual void setSamplesPerBlock(unsigned count) { (void)count; } + + /** + * @brief Initialize the generator. + * + * @param sourceKey identifier of the source to initialize + * @param voiceId the particular voice to initialize, if per-voice + */ + virtual void init(const ModKey& sourceKey, NumericId 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; + + /** + * @brief Advance the generator by a number of frames + * This is called instead of `generate` in case the output is discarded. + * It can be overriden with a faster implementation if wanted. + * + * @param sourceKey source key + * @param voiceNum voice number if the generator is per-voice, otherwise undefined + * @param buffer writable spare buffer, contents will be discarded + */ + virtual void generateDiscarded(const ModKey& sourceKey, NumericId voiceNum, absl::Span buffer) + { + generate(sourceKey, voiceNum, buffer); + } +}; + +} // 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..5ba5e02c --- /dev/null +++ b/src/sfizz/modulations/ModKey.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModKey.h" +#include "ModId.h" +#include "../Debug.h" +#include +#include + +namespace sfz { + +ModKey::Parameters::Parameters() noexcept +{ + // zero-fill the structure + // 1. this ensures that non-used values will be always 0 + // 2. this makes the object memcmp-comparable + std::memset(this, 0, sizeof(*this)); +} + +ModKey::Parameters::Parameters(const Parameters& other) noexcept +{ + std::memcpy(this, &other, sizeof(*this)); +} + +ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexcept +{ + if (this != &other) + std::memcpy(this, &other, sizeof(*this)); + return *this; +} + +bool ModKey::Parameters::operator==(const Parameters& other) const noexcept +{ + return std::memcmp(this, &other, sizeof(*this)) == 0; +} + +bool ModKey::Parameters::operator!=(const Parameters& other) const noexcept +{ + return std::memcmp(this, &other, sizeof(*this)) != 0; +} + +ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) +{ + ModKey::Parameters p; + p.cc = cc; + p.curve = curve; + p.smooth = smooth; + p.value = value; + p.step = step; + return ModKey(ModId::Controller, {}, p); +} + +ModKey ModKey::createNXYZ(ModId id, NumericId region, uint8_t N, uint8_t X, uint8_t Y, uint8_t Z) +{ + ASSERT(id != ModId::Controller); + ModKey::Parameters p; + p.N = N; + p.X = X; + p.Y = Y; + p.Z = Z; + return ModKey(id, region, p); +} + +bool ModKey::isSource() const noexcept +{ + return ModIds::isSource(id_); +} + +bool ModKey::isTarget() const noexcept +{ + return ModIds::isTarget(id_); +} + +int ModKey::flags() const noexcept +{ + return ModIds::flags(id_); +} + +std::string ModKey::toString() const +{ + switch (id_) { + case ModId::Controller: + return absl::StrCat("Controller ", params_.cc, + " {curve=", params_.curve, ", smooth=", params_.smooth, + ", value=", params_.value, ", step=", params_.step, "}"); + case ModId::Envelope: + return absl::StrCat("EG ", 1 + params_.N); + case ModId::LFO: + return absl::StrCat("LFO ", 1 + params_.N); + + case ModId::Amplitude: + return "Amplitude"; + case ModId::Pan: + return "Pan"; + case ModId::Width: + return "Width"; + case ModId::Position: + return "Position"; + case ModId::Pitch: + return "Pitch"; + case ModId::Volume: + return "Volume"; + + default: + return {}; + } +} + +} // namespace sfz + +bool sfz::ModKey::operator==(const ModKey &other) const noexcept +{ + return id_ == other.id_ && region_ == other.region_ && + parameters() == other.parameters(); +} + +bool sfz::ModKey::operator!=(const ModKey &other) const noexcept +{ + return !this->operator==(other); +} diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h new file mode 100644 index 00000000..37b8b072 --- /dev/null +++ b/src/sfizz/modulations/ModKey.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "ModKeyHash.h" +#include "../NumericId.h" +#include + +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(uint16_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() noexcept; + Parameters(const Parameters& other) noexcept; + Parameters& operator=(const Parameters& other) noexcept; + + Parameters(Parameters&&) = delete; + Parameters &operator=(Parameters&&) = delete; + + bool operator==(const Parameters& other) const noexcept; + bool operator!=(const Parameters& other) const noexcept; + + union { + //! Parameters if this key identifies a CC source + struct { uint16_t cc; uint8_t curve, smooth; float value, step; }; + //! Parameters otherwise, based on the related opcode + // eg. `N` in `lfoN`, `N, X` in `lfoN_eqX` + struct { uint8_t N, X, Y, Z; }; + // !!! NOTE: NXYZ is expected to be stored in 0-indexed form + // eg. `lfo1_eq2` is N=0, X=1 + }; + }; + +public: + bool operator==(const ModKey &other) const noexcept; + bool operator!=(const ModKey &other) const noexcept; + +private: + //! Identifier + ModId id_ {}; + //! Region identifier, only applicable if the modulation is per-voice + NumericId region_; + //! 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..1f3ecbdc --- /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 +#include + +namespace sfz { class ModKey; } + +namespace std { + 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..41b1c844 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModMatrix.h" +#include "ModId.h" +#include "ModKey.h" +#include "ModGenerator.h" +#include "Buffer.h" +#include "Config.h" +#include "SIMDHelpers.h" +#include "Debug.h" +#include +#include +#include +#include + +namespace sfz { + +struct ModMatrix::Impl { + double sampleRate_ {}; + uint32_t samplesPerBlock_ {}; + + uint32_t numFrames_ {}; + NumericId currentVoiceId_ {}; + NumericId currentRegionId_ {}; + + struct Source { + ModKey key; + ModGenerator* gen {}; + bool bufferReady {}; + Buffer buffer; + }; + + struct ConnectionData { + float sourceDepth_ {}; + }; + + 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_; +}; + +ModMatrix::ModMatrix() + : impl_(new Impl) +{ + setSampleRate(config::defaultSampleRate); + setSamplesPerBlock(config::defaultSamplesPerBlock); +} + +ModMatrix::~ModMatrix() +{ +} + +void ModMatrix::clear() +{ + Impl& impl = *impl_; + + impl.sourceIndex_.clear(); + impl.targetIndex_.clear(); + impl.sources_.clear(); + impl.targets_.clear(); +} + +void ModMatrix::setSampleRate(double sampleRate) +{ + Impl& impl = *impl_; + + if (impl.sampleRate_ == sampleRate) + return; + + impl.sampleRate_ = sampleRate; + + for (Impl::Source &source : impl.sources_) + source.gen->setSampleRate(sampleRate); +} + +void ModMatrix::setSamplesPerBlock(unsigned samplesPerBlock) +{ + Impl& impl = *impl_; + + if (impl.samplesPerBlock_ == samplesPerBlock) + return; + + impl.samplesPerBlock_ = samplesPerBlock; + + for (Impl::Source &source : impl.sources_) { + source.buffer.resize(samplesPerBlock); + source.gen->setSamplesPerBlock(samplesPerBlock); + } + for (Impl::Target &target : impl.targets_) + target.buffer.resize(samplesPerBlock); +} + +ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& gen) +{ + Impl& impl = *impl_; + + auto it = impl.sourceIndex_.find(key); + if (it != impl.sourceIndex_.end()) { + ASSERT(&gen == impl.sources_[it->second].gen); + return SourceId(it->second); + } + + SourceId id(static_cast(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, float sourceDepth) +{ + Impl& impl = *impl_; + unsigned sourceIndex = sourceId.number(); + unsigned targetIndex = targetId.number(); + + if (sourceIndex >= impl.sources_.size() || targetIndex >= impl.targets_.size()) + return false; + + Impl::Target& target = impl.targets_[targetIndex]; + Impl::ConnectionData& conn = target.connectedSources[sourceIndex]; + conn.sourceDepth_ = sourceDepth; + + return true; +} + +void ModMatrix::init() +{ + Impl& impl = *impl_; + + for (Impl::Source &source : impl.sources_) { + const int flags = source.key.flags(); + if (flags & kModIsPerCycle) + source.gen->init(source.key, {}); + } +} + +void ModMatrix::initVoice(NumericId voiceId, NumericId regionId) +{ + Impl& impl = *impl_; + + for (Impl::Source &source : impl.sources_) { + const int flags = source.key.flags(); + if ((flags & kModIsPerVoice) && source.key.region() == regionId) + source.gen->init(source.key, voiceId); + } +} + +void ModMatrix::beginCycle(unsigned numFrames) +{ + Impl& impl = *impl_; + + impl.numFrames_ = numFrames; + + for (Impl::Source &source : impl.sources_) + source.bufferReady = false; + for (Impl::Target &target : impl.targets_) + target.bufferReady = false; +} + +void ModMatrix::endCycle() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + const int flags = source.key.flags(); + if (flags & kModIsPerCycle) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, {}, buffer); + } + } + } + + impl.numFrames_ = 0; +} + +void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) +{ + Impl& impl = *impl_; + + impl.currentVoiceId_ = voiceId; + impl.currentRegionId_ = regionId; + + for (Impl::Source &source : impl.sources_) { + const int flags = source.key.flags(); + if (flags & kModIsPerVoice) + source.bufferReady = false; + } + for (Impl::Target &target : impl.targets_) { + const int flags = target.key.flags(); + if (flags & kModIsPerVoice) + target.bufferReady = false; + } +} + +void ModMatrix::endVoice() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + const NumericId voiceId = impl.currentVoiceId_; + const NumericId regionId = impl.currentRegionId_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + const int flags = source.key.flags(); + if ((flags & kModIsPerVoice) && source.key.region() == regionId) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, voiceId, buffer); + } + } + } + + impl.currentVoiceId_ = {}; + impl.currentRegionId_ = {}; +} + +float* ModMatrix::getModulation(TargetId targetId) +{ + if (!validTarget(targetId)) + return nullptr; + + Impl& impl = *impl_; + const NumericId regionId = impl.currentRegionId_; + const uint32_t targetIndex = targetId.number(); + Impl::Target &target = impl.targets_[targetIndex]; + const int targetFlags = target.key.flags(); + + const uint32_t numFrames = impl.numFrames_; + absl::Span buffer(target.buffer.data(), numFrames); + + // only accept per-voice targets of the same region + if ((targetFlags & kModIsPerVoice) && regionId != target.key.region()) + return nullptr; + + // check if already processed + if (target.bufferReady) + return buffer.data(); + + // set the ready flag to prevent a cycle + // in case there is, be sure to initialize the buffer + target.bufferReady = true; + + auto sourcesPos = target.connectedSources.begin(); + auto sourcesEnd = target.connectedSources.end(); + bool isFirstSource = true; + + // generate sources in their dedicated buffers + // then add or multiply, depending on target flags + while (sourcesPos != sourcesEnd) { + Impl::Source &source = impl.sources_[sourcesPos->first]; + const float sourceDepth = sourcesPos->second.sourceDepth_; + const int sourceFlags = source.key.flags(); + + // only accept per-voice sources of the same region + bool useThisSource = true; + if (sourceFlags & kModIsPerVoice) + useThisSource = (regionId == source.key.region()); + + if (useThisSource) { + absl::Span sourceBuffer(source.buffer.data(), numFrames); + + // unless source is already done, process it + if (!source.bufferReady) { + source.gen->generate(source.key, impl.currentVoiceId_, sourceBuffer); + source.bufferReady = true; + } + + if (isFirstSource) { + if (sourceDepth != 1) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] = sourceDepth * sourceBuffer[i]; + } + else { + copy(absl::Span(sourceBuffer), buffer); + } + isFirstSource = false; + } + else { + if (targetFlags & kModIsMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= sourceDepth * sourceBuffer[i]; + } + else if (targetFlags & kModIsPercentMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= (0.01f * sourceDepth) * sourceBuffer[i]; + } + else { + ASSERT(targetFlags & kModIsAdditive); + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] += sourceDepth * sourceBuffer[i]; + } + } + } + + ++sourcesPos; + } + + // if there were no source, fill output with the neutral element + if (isFirstSource) { + if (targetFlags & kModIsMultiplicative) + fill(buffer, 1.0f); + else if (targetFlags & kModIsPercentMultiplicative) + fill(buffer, 100.0f); + else { + ASSERT(targetFlags & kModIsAdditive); + fill(buffer, 0.0f); + } + } + + return buffer.data(); +} + +bool ModMatrix::validTarget(TargetId id) const +{ + return static_cast(id.number()) < impl_->targets_.size(); +} + +bool ModMatrix::validSource(SourceId id) const +{ + return static_cast(id.number()) < impl_->sources_.size(); +} + +std::string ModMatrix::toDotGraph() const +{ + const Impl& impl = *impl_; + + struct Edge { + std::string source; + std::string target; + }; + + // collect all connections as string pairs + std::vector edges; + for (const Impl::Target& target : impl.targets_) { + for (const auto& cs : target.connectedSources) { + const Impl::Source& source = impl.sources_[cs.first]; + Edge e; + e.source = source.key.toString(); + e.target = target.key.toString(); + edges.push_back(std::move(e)); + } + } + + // alphabetic sort, to produce stable output for unit testing + auto compare = [](const Edge& a, const Edge& b) -> bool { + std::pair aa{a.source, a.target}; + std::pair bb{b.source, b.target}; + return aa < bb; + }; + std::sort(edges.begin(), edges.end(), compare); + + // write dot graph + std::string dot; + dot.reserve(1024); + absl::StrAppend(&dot, "digraph {" "\n"); + for (const Edge& e : edges) { + absl::StrAppend(&dot, "\t" "\"", e.source, "\"" + " -> " "\"", e.target, "\"" "\n"); + } + absl::StrAppend(&dot, "}" "\n"); + return dot; +} + +} // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h new file mode 100644 index 00000000..fa1f2633 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.h @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../NumericId.h" +#include +#include +#include + +namespace sfz { + +class ModKey; +class ModGenerator; +class Voice; +struct Region; + +/** + * @brief Modulation matrix + */ +class ModMatrix { +public: + ModMatrix(); + ~ModMatrix(); + + struct SourceIdTag; + struct TargetIdTag; + + //! Identifier of a modulation source + typedef NumericId 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 + * @param sourceDepth amount which multiplies the source output + * @return true if the connection was successfully made, otherwise false + */ + bool connect(SourceId sourceId, TargetId targetId, float sourceDepth); + + /** + * @brief Reinitialize modulation sources overall. + * This must be called once after setting up the matrix. + */ + void init(); + + /** + * @brief Reinitialize modulation source for a given voice. + * This must be called first after a voice enters active state. + */ + void initVoice(NumericId voiceId, NumericId regionId); + + /** + * @brief Start modulation processing for the entire cycle. + * This clears all the buffers. + * + * @param numFrames + */ + void beginCycle(unsigned numFrames); + + /** + * @brief End modulation processing for the entire cycle. + * This performs a dummy run of any unused modulations. + */ + void endCycle(); + + /** + * @brief Start modulation processing for a given voice. + * This clears all the buffers which are per-voice. + * + * @param voiceId the identifier of the current voice + * @param regionId the identifier of the region of the current voice + */ + void beginVoice(NumericId voiceId, NumericId regionId); + + /** + * @brief End modulation processing for a given voice. + * This performs a dummy run of any unused modulations which are per-cycle. + */ + void endVoice(); + + /** + * @brief Get the modulation buffer for the given target. + * If the target does not exist, the result is null. + * + * @param targetId identifier of the modulation target + */ + float* getModulation(TargetId targetId); + + /** + * @brief Get the modulation buffer for the given target. + * Same as `getModulation`, but accepting a key directly. + * + * @param targetKey key of the modulation target + */ + float* getModulationByKey(const ModKey& targetKey) + { return getModulation(findTarget(targetKey)); } + + /** + * @brief Return whether the target identifier is valid. + * + * @param id + */ + bool validTarget(TargetId id) const; + + /** + * @brief Return whether the source identifier is valid. + * + * @param id + */ + bool validSource(SourceId id) const; + + /** + * @brief Get a representation of the matrix written as a Dot graph. + */ + std::string toDotGraph() const; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp new file mode 100644 index 00000000..692b7556 --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Controller.h" +#include "../ModKey.h" +#include "../../Smoothers.h" +#include "../../ModifierHelpers.h" +#include "../../Resources.h" +#include "../../Config.h" +#include "../../Debug.h" +#include + +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; + bool canShortcut = events.size() == 1; + 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..4018edee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,8 @@ project(sfizz) set(SFIZZ_TEST_SOURCES RegionT.cpp + RegionTHelpers.h + RegionTHelpers.cpp ParsingT.cpp HelpersT.cpp HelpersT.cpp @@ -36,6 +38,7 @@ set(SFIZZ_TEST_SOURCES SwapAndPopT.cpp TuningT.cpp ConcurrencyT.cpp + ModulationsT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 64bcb7e4..01287d3e 100644 --- a/tests/EventEnvelopesT.cpp +++ b/tests/EventEnvelopesT.cpp @@ -261,6 +261,7 @@ TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps") REQUIRE(approxEqual(output, expected)); } +#if 0 TEST_CASE("[linearModifiers] Compare with envelopes") { sfz::Resources resources; @@ -360,4 +361,4 @@ TEST_CASE("[multiplicativeModifiers] Compare with envelopes") }); REQUIRE(approxEqual(output, envelope)); } - +#endif diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 73ea4ab0..b47181fe 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -4,8 +4,11 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "RegionTHelpers.h" #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" #include "catch2/catch.hpp" #include "ghc/fs_std.hpp" #if defined(__APPLE__) @@ -356,9 +359,11 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines") REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); - REQUIRE(!synth.getRegionView(2)->modifiers[Mod::amplitude].empty()); - REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].contains(10)); - REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].getWithDefault(10).value == 34.0f); + + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, synth.getRegionView(2)->getId()); + const RegionCCView view(*synth.getRegionView(2), target); + REQUIRE(!view.empty()); + REQUIRE(view.at(10).value == 34.0f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp new file mode 100644 index 00000000..709ec36d --- /dev/null +++ b/tests/ModulationsT.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" +#include "sfizz/Synth.h" +#include "catch2/catch.hpp" + +TEST_CASE("[Modulations] Identifiers") +{ + // check that modulations are well defined as either source and target + // and all targets have their default value defined + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + REQUIRE(sfz::ModIds::isSource(id)); + REQUIRE(!sfz::ModIds::isTarget(id)); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + REQUIRE(sfz::ModIds::isTarget(id)); + REQUIRE(!sfz::ModIds::isSource(id)); + }); +} + +TEST_CASE("[Modulations] Flags") +{ + // check validity of modulation flags + + static auto* checkBasicFlags = +[](int flags) + { + REQUIRE(flags != sfz::kModFlagsInvalid); + REQUIRE((bool(flags & sfz::kModIsPerCycle) + + bool(flags & sfz::kModIsPerVoice)) == 1); + }; + static auto* checkSourceFlags = +[](int flags) + { + checkBasicFlags(flags); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 0); + }; + static auto* checkTargetFlags = +[](int flags) + { + checkBasicFlags(flags); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 1); + }; + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + checkSourceFlags(sfz::ModIds::flags(id)); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + checkTargetFlags(sfz::ModIds::flags(id)); + }); +} + +TEST_CASE("[Modulations] Display names") +{ + // check all modulations are implemented in `toString` + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + REQUIRE(!sfz::ModKey(id).toString().empty()); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + REQUIRE(!sfz::ModKey(id).toString().empty()); + }); +} + +TEST_CASE("[Modulations] Connection graph from SFZ") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + +sample=*sine +amplitude_oncc20=59 amplitude_curvecc20=3 +pitch_oncc42=71 pitch_smoothcc42=32 +pan_oncc36=14.5 pan_stepcc36=1.5 +width_oncc425=29 +)"); + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == R"(digraph { + "Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude" + "Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan" + "Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch" + "Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width" +} +)"); +} diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index ecfea499..472ab8fc 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -4,10 +4,14 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "RegionTHelpers.h" #include "sfizz/MidiState.h" #include "sfizz/Region.h" #include "sfizz/SfzHelpers.h" +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" #include "catch2/catch.hpp" +#include using namespace Catch::literals; using namespace sfz::literals; using namespace sfz; @@ -541,28 +545,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("pan_oncc") { - REQUIRE(region.modifiers[Mod::pan].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Pan, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "pan_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::pan].contains(45)); - REQUIRE(region.modifiers[Mod::pan][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "pan_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pan_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "pan_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "pan_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "pan_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "pan_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "pan_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "pan_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "pan_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("width") @@ -580,28 +585,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("width_oncc") { - REQUIRE(region.modifiers[Mod::width].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Width, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "width_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::width].contains(45)); - REQUIRE(region.modifiers[Mod::width][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "width_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "width_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "width_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "width_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "width_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "width_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "width_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::width][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "width_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::width][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "width_stepcc120", "-20" }); - REQUIRE(region.modifiers[Mod::width][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("position") @@ -619,28 +625,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("position_oncc") { - REQUIRE(region.modifiers[Mod::position].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Position, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "position_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::position].contains(45)); - REQUIRE(region.modifiers[Mod::position][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "position_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "position_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "position_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "position_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "position_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "position_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "position_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::position][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "position_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::position][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "position_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::position][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("amp_keycenter") @@ -1641,95 +1648,93 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("amplitude_cc") { - REQUIRE(region.modifiers[Mod::amplitude].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "amplitude_cc1", "40" }); - REQUIRE(region.modifiers[Mod::amplitude].contains(1)); - REQUIRE(region.modifiers[Mod::amplitude][1].value == 40.0_a); + REQUIRE(view.at(1).value == 40.0_a); region.parseOpcode({ "amplitude_oncc2", "30" }); - REQUIRE(region.modifiers[Mod::amplitude].contains(2)); - REQUIRE(region.modifiers[Mod::amplitude][2].value == 30.0_a); + REQUIRE(view.at(2).value == 30.0_a); region.parseOpcode({ "amplitude_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "amplitude_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "amplitude_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "amplitude_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "amplitude_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "amplitude_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "amplitude_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "amplitude_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 100.0_a); + REQUIRE(view.at(120).step == 100.0_a); region.parseOpcode({ "amplitude_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("volume_oncc/gain_cc") { - REQUIRE(region.modifiers[Mod::volume].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Volume, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "gain_cc1", "40" }); - REQUIRE(region.modifiers[Mod::volume].contains(1)); - REQUIRE(region.modifiers[Mod::volume][1].value == 40_a); + REQUIRE(view.at(1).value == 40_a); region.parseOpcode({ "volume_oncc2", "-76" }); - REQUIRE(region.modifiers[Mod::volume].contains(2)); - REQUIRE(region.modifiers[Mod::volume][2].value == -76.0_a); + REQUIRE(view.at(2).value == -76.0_a); region.parseOpcode({ "gain_oncc4", "-1" }); - REQUIRE(region.modifiers[Mod::volume].contains(4)); - REQUIRE(region.modifiers[Mod::volume][4].value == -1.0_a); + REQUIRE(view.at(4).value == -1.0_a); region.parseOpcode({ "volume_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "volume_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "volume_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "volume_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "volume_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "volume_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "volume_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 24.0f); + REQUIRE(view.at(120).step == 24.0f); region.parseOpcode({ "volume_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 144.0f); + REQUIRE(view.at(120).step == 144.0f); region.parseOpcode({ "volume_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("tune_cc/pitch_cc") { - REQUIRE(region.modifiers[Mod::pitch].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Pitch, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "pitch_cc1", "40" }); - REQUIRE(region.modifiers[Mod::pitch].contains(1)); - REQUIRE(region.modifiers[Mod::pitch][1].value == 40.0); + REQUIRE(view.at(1).value == 40.0); region.parseOpcode({ "tune_oncc2", "-76" }); - REQUIRE(region.modifiers[Mod::pitch].contains(2)); - REQUIRE(region.modifiers[Mod::pitch][2].value == -76.0); + REQUIRE(view.at(2).value == -76.0); region.parseOpcode({ "pitch_oncc4", "-1" }); - REQUIRE(region.modifiers[Mod::pitch].contains(4)); - REQUIRE(region.modifiers[Mod::pitch][4].value == -1.0); + REQUIRE(view.at(4).value == -1.0); region.parseOpcode({ "tune_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pitch_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "tune_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "pitch_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "tune_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "pitch_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "tune_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 24.0f); + REQUIRE(view.at(120).step == 24.0f); region.parseOpcode({ "pitch_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 9600.0f); + REQUIRE(view.at(120).step == 9600.0f); region.parseOpcode({ "tune_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } } diff --git a/tests/RegionTHelpers.cpp b/tests/RegionTHelpers.cpp new file mode 100644 index 00000000..a47b56ed --- /dev/null +++ b/tests/RegionTHelpers.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "RegionTHelpers.h" +#include "sfizz/modulations/ModId.h" + +size_t RegionCCView::size() const +{ + size_t count = 0; + for (const sfz::Region::Connection& conn : region_.connections) + count += match(conn); + return count; +} + +bool RegionCCView::empty() const +{ + for (const sfz::Region::Connection& conn : region_.connections) + if (match(conn)) + return false; + return true; +} + +sfz::ModKey::Parameters RegionCCView::at(int cc) const +{ + for (const sfz::Region::Connection& conn : region_.connections) { + if (match(conn)) { + const sfz::ModKey::Parameters p = conn.source.parameters(); + if (p.cc == cc) + return p; + } + } + throw std::out_of_range("Region CC"); +} + +bool RegionCCView::match(const sfz::Region::Connection& conn) const +{ + return conn.source.id() == sfz::ModId::Controller && conn.target == target_; +} diff --git a/tests/RegionTHelpers.h b/tests/RegionTHelpers.h new file mode 100644 index 00000000..e9fcf897 --- /dev/null +++ b/tests/RegionTHelpers.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "sfizz/Region.h" +#include "sfizz/modulations/ModKey.h" + +class RegionCCView { +public: + RegionCCView(const sfz::Region& region, sfz::ModKey target) + : region_(region), target_(target) + { + } + + size_t size() const; + bool empty() const; + sfz::ModKey::Parameters at(int cc) const; + +private: + bool match(const sfz::Region::Connection& conn) const; + +private: + const sfz::Region& region_; + sfz::ModKey target_; +};