From 675d7f3fc5ea234e78aa27bb65bdfbb4cba32ac6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 19:25:59 +0200 Subject: [PATCH 01/20] 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()); + }); +} From 0ebe24939d04f2620f09a87e5055dc81ca6b890c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 16:23:28 +0200 Subject: [PATCH 02/20] CC number oopsie --- src/sfizz/modulations/ModKey.cpp | 2 +- src/sfizz/modulations/ModKey.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 3a865bd7..fc60df9c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -12,7 +12,7 @@ namespace sfz { -ModKey ModKey::createCC(uint8_t cc, uint8_t curve, uint8_t smooth, float value, float step) +ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) { ModKey::Parameters p; p.cc = cc; diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index e70c244d..3fd5d470 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -27,7 +27,7 @@ public: 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 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(); } @@ -45,7 +45,7 @@ public: 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; }; + 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; }; From 82ebb639a76f3eb98e2f9f88c3c4aac7a9256cbe Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 17:32:25 +0200 Subject: [PATCH 03/20] Remove unused code, update tests --- src/CMakeLists.txt | 1 - src/sfizz/ModifierHelpers.h | 78 +---------------- src/sfizz/Modifiers.h | 92 -------------------- src/sfizz/Opcode.h | 2 +- src/sfizz/Region.cpp | 39 ++------- src/sfizz/Region.h | 8 +- src/sfizz/Synth.cpp | 5 -- src/sfizz/Synth.h | 5 +- src/sfizz/Voice.cpp | 48 ---------- src/sfizz/Voice.h | 24 ----- tests/CMakeLists.txt | 2 + tests/EventEnvelopesT.cpp | 3 +- tests/FilesT.cpp | 11 ++- tests/RegionT.cpp | 169 +++++++++++++++++++----------------- tests/RegionTHelpers.cpp | 41 +++++++++ tests/RegionTHelpers.h | 28 ++++++ 16 files changed, 182 insertions(+), 374 deletions(-) delete mode 100644 src/sfizz/Modifiers.h create mode 100644 tests/RegionTHelpers.cpp create mode 100644 tests/RegionTHelpers.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0343c5e..a1199157 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,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 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 c8775c0b..685d7587 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -379,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], ModKey::createNXYZ(ModId::Volume, id)); + 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], ModKey::createNXYZ(ModId::Amplitude, id)); + 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], ModKey::createNXYZ(ModId::Pan, id)); + 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], ModKey::createNXYZ(ModId::Position, id)); + 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], ModKey::createNXYZ(ModId::Width, id)); + processGenericCc(opcode, Default::widthCCRange, ModKey::createNXYZ(ModId::Width, id)); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -771,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], ModKey::createNXYZ(ModId::Pitch, id)); + processGenericCc(opcode, Default::tuneCCRange, ModKey::createNXYZ(ModId::Pitch, id)); break; case hash("bend_up"): // also bendup setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -925,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } -bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target) +bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, const ModKey& target) { if (!opcode.isAnyCcN()) return false; @@ -934,31 +934,6 @@ 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) { - case kOpcodeOnCcN: - setValueFromOpcode(opcode, modifier.value, range); - break; - case kOpcodeCurveCcN: - setValueFromOpcode(opcode, modifier.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); - } - break; - case kOpcodeSmoothCcN: - setValueFromOpcode(opcode, modifier.smooth, Default::smoothCCRange); - break; - default: - assert(false); - break; - } - } - if (target) { // search an existing connection of same CC number and target // if it exists, modify, otherwise create diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index da044fd9..0426d6d7 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -17,9 +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 @@ -241,12 +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, const ModKey& target); + bool processGenericCc(const Opcode& opcode, Range range, const ModKey& target); void offsetAllKeys(int offset) noexcept; @@ -370,9 +369,6 @@ 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 }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7bae9797..cb1ba8c4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -452,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(); @@ -563,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; } @@ -574,7 +571,6 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; - settingsPerVoice.maxModifiers = maxModifiers; applySettingsPerVoice(); @@ -1346,7 +1342,6 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); - voice->prepareSmoothers(settingsPerVoice.maxModifiers); } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 5c41078d..8133b676 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -18,11 +18,11 @@ #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 { @@ -772,7 +772,6 @@ private: 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 a9ced8b5..9f1b9197 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -141,33 +141,6 @@ 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); } @@ -882,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(); @@ -918,21 +885,6 @@ void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept 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/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9b7155c5..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 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/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..d55f05ad --- /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.first.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.first.id() == sfz::ModId::Controller && conn.second == 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_; +}; From efd616ffdfa03253473d11a9f313ced6c45185e0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 18:38:36 +0200 Subject: [PATCH 04/20] Ensure all ModKey to have unused content set to zero --- src/sfizz/modulations/ModKey.cpp | 32 +++++++++++++++++++++++++++++++- src/sfizz/modulations/ModKey.h | 12 ++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index fc60df9c..57038748 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -12,6 +12,36 @@ 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; @@ -84,7 +114,7 @@ std::string ModKey::toString() const bool sfz::ModKey::operator==(const ModKey &other) const noexcept { return id_ == other.id_ && region_ && other.region_ && - !std::memcmp(¶meters(), &other.parameters(), sizeof(ModKey::Parameters)); + parameters() == other.parameters(); } bool sfz::ModKey::operator!=(const ModKey &other) const noexcept diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 3fd5d470..48977537 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -8,7 +8,6 @@ #include "ModKeyHash.h" #include "../NumericId.h" #include -#include namespace sfz { @@ -42,7 +41,16 @@ public: std::string toString() const; struct Parameters { - Parameters() { std::memset(this, 0, sizeof(*this)); } + 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; }; From 6bb4b7c83e86442513b507905b0b62ad825c6914 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 18:47:09 +0200 Subject: [PATCH 05/20] Fix equality comparison --- src/sfizz/modulations/ModKey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 57038748..69d8750f 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -113,7 +113,7 @@ std::string ModKey::toString() const bool sfz::ModKey::operator==(const ModKey &other) const noexcept { - return id_ == other.id_ && region_ && other.region_ && + return id_ == other.id_ && region_ == other.region_ && parameters() == other.parameters(); } From 97702e43081c0a2fdfea3bce0ad4b91a0bb25422 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 19:14:30 +0200 Subject: [PATCH 06/20] Advance the generator when not used --- src/sfizz/Synth.cpp | 8 +++++-- src/sfizz/modulations/ModGenerator.h | 14 ++++++++++++ src/sfizz/modulations/ModMatrix.cpp | 33 ++++++++++++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 12 ++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index cb1ba8c4..cba7e139 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -723,6 +723,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept } ModMatrix& mm = resources.modMatrix; + mm.beginCycle(numFrames); activeVoices = 0; { // Main render block @@ -731,8 +732,6 @@ 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) { @@ -754,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(); } @@ -781,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()); diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index c146593d..457460c3 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -47,6 +47,20 @@ public: * @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/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index dd1bd4c3..9dd94893 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -224,6 +224,22 @@ void ModMatrix::beginCycle(unsigned numFrames) target.bufferReady = false; } +void ModMatrix::endCycle() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + int flags = source.key.flags(); + if (flags & kModIsPerCycle) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, {}, buffer); + } + } + } +} + void ModMatrix::beginVoice(NumericId voiceId) { Impl& impl = *impl_; @@ -242,6 +258,23 @@ void ModMatrix::beginVoice(NumericId voiceId) } } +void ModMatrix::endVoice() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + const NumericId voiceId = impl.voiceId_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + int flags = source.key.flags(); + if (flags & kModIsPerVoice) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, voiceId, buffer); + } + } + } +} + float* ModMatrix::getModulation(TargetId targetId) { if (!validTarget(targetId)) diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 34edb802..e3381930 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -113,6 +113,12 @@ public: */ 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. @@ -121,6 +127,12 @@ public: */ void beginVoice(NumericId voiceId); + /** + * @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. From 5835255d07bfa7c7023e6e334e05885ada88f09f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 20:19:22 +0200 Subject: [PATCH 07/20] Fix for macOS --- src/sfizz/modulations/ModKeyHash.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKeyHash.h b/src/sfizz/modulations/ModKeyHash.h index a5cdf4c6..1f3ecbdc 100644 --- a/src/sfizz/modulations/ModKeyHash.h +++ b/src/sfizz/modulations/ModKeyHash.h @@ -5,12 +5,12 @@ // 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; template <> struct hash { size_t operator()(const sfz::ModKey &key) const; }; From 652d0c898d84ee9380205ec857c734316ac8964e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 21:00:13 +0200 Subject: [PATCH 08/20] Enable smoothing for CC --- src/sfizz/modulations/sources/Controller.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index b60269f0..692b7556 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -86,10 +86,7 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI auto it = impl_->smoother_.find(sourceKey); if (it != impl_->smoother_.end()) { Smoother& s = it->second; - - #pragma message("TODO: implement CC shortcut") - bool canShortcut = false; - + bool canShortcut = events.size() == 1; s.process(buffer, buffer, canShortcut); } } From 0507dab7c4dc3fb7bf30c159792efc07873d92fd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:30:10 +0200 Subject: [PATCH 09/20] Ensure to only generate per-voice modulations of the same region --- src/sfizz/Synth.cpp | 2 +- src/sfizz/modulations/ModMatrix.cpp | 99 ++++++++++++++++++----------- src/sfizz/modulations/ModMatrix.h | 4 +- 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index cba7e139..61fb532d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -744,7 +744,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; - mm.beginVoice(voice->getId()); + mm.beginVoice(voice->getId(), voice->getRegion()->getId()); activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 9dd94893..74fa25cd 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -24,6 +24,7 @@ struct ModMatrix::Impl { uint32_t numFrames_ {}; NumericId voiceId_ {}; + NumericId regionId_ {}; struct Source { ModKey key; @@ -195,7 +196,7 @@ void ModMatrix::init() Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { - int flags = source.key.flags(); + const int flags = source.key.flags(); if (flags & kModIsPerCycle) source.gen->init(source.key, {}); } @@ -206,7 +207,7 @@ void ModMatrix::initVoice(NumericId voiceId) Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { - int flags = source.key.flags(); + const int flags = source.key.flags(); if (flags & kModIsPerVoice) source.gen->init(source.key, voiceId); } @@ -231,20 +232,23 @@ void ModMatrix::endCycle() for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { - int flags = source.key.flags(); + 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) +void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) { Impl& impl = *impl_; impl.voiceId_ = voiceId; + impl.regionId_ = regionId; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); @@ -263,16 +267,20 @@ void ModMatrix::endVoice() Impl& impl = *impl_; const uint32_t numFrames = impl.numFrames_; const NumericId voiceId = impl.voiceId_; + const NumericId regionId = impl.regionId_; for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { - int flags = source.key.flags(); - if (flags & kModIsPerVoice) { + 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.voiceId_ = {}; + impl.regionId_ = {}; } float* ModMatrix::getModulation(TargetId targetId) @@ -281,13 +289,18 @@ float* ModMatrix::getModulation(TargetId targetId) return nullptr; Impl& impl = *impl_; + const NumericId regionId = impl.regionId_; const uint32_t targetIndex = targetId.number(); Impl::Target &target = impl.targets_[targetIndex]; - const int flags = target.key.flags(); + 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(); @@ -295,45 +308,59 @@ float* ModMatrix::getModulation(TargetId targetId) // 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(); + bool isFirstSource = true; - // generate the first source in buffer - if (sourcesPos != sourcesEnd) { + // generate first source in output buffer, next sources in temporary buffer + // then add or multiply, depending on target flags + while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; - source.gen->generate(source.key, impl.voiceId_, buffer); + 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) { + if (isFirstSource) { + source.gen->generate(source.key, impl.voiceId_, buffer); + isFirstSource = false; + } + else { + absl::Span temp(impl.temp_.data(), numFrames); + source.gen->generate(source.key, impl.voiceId_, temp); + if (targetFlags & kModIsMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= temp[i]; + } + else if (targetFlags & kModIsPercentMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= 0.01f * temp[i]; + } + else { + ASSERT(targetFlags & kModIsAdditive); + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] += temp[i]; + } + } + } + ++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]; - } + // if there were no source, fill output with the neutral element + if (isFirstSource) { + if (targetFlags & kModIsMultiplicative) + sfz::fill(buffer, 1.0f); + else if (targetFlags & kModIsPercentMultiplicative) + sfz::fill(buffer, 100.0f); else { - ASSERT(flags & kModIsAdditive); - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += temp[i]; + ASSERT(targetFlags & kModIsAdditive); + sfz::fill(buffer, 0.0f); } - ++sourcesPos; } return buffer.data(); diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index e3381930..7cf51e6f 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -14,6 +14,7 @@ namespace sfz { class ModKey; class ModGenerator; class Voice; +struct Region; /** * @brief Modulation matrix @@ -124,8 +125,9 @@ public: * 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); + void beginVoice(NumericId voiceId, NumericId regionId); /** * @brief End modulation processing for a given voice. From 4703b939ab3ef6043ea3b92c11e169bab7533ce7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:37:08 +0200 Subject: [PATCH 10/20] Only init per-voice modulations which are of the region --- src/sfizz/Voice.cpp | 2 +- src/sfizz/modulations/ModMatrix.cpp | 4 ++-- src/sfizz/modulations/ModMatrix.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 9f1b9197..97fa044d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -141,7 +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); - resources.modMatrix.initVoice(id); + resources.modMatrix.initVoice(id, region->getId()); } int sfz::Voice::getCurrentSampleQuality() const noexcept diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 74fa25cd..c4893fd0 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -202,13 +202,13 @@ void ModMatrix::init() } } -void ModMatrix::initVoice(NumericId voiceId) +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) + if ((flags & kModIsPerVoice) && source.key.region() == regionId) source.gen->init(source.key, voiceId); } } diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 7cf51e6f..4622f4b1 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -104,7 +104,7 @@ public: * @brief Reinitialize modulation source for a given voice. * This must be called first after a voice enters active state. */ - void initVoice(NumericId voiceId); + void initVoice(NumericId voiceId, NumericId regionId); /** * @brief Start modulation processing for the entire cycle. From 0b20932e12507ee43ce570a49c770e0bfa68c30f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:52:38 +0200 Subject: [PATCH 11/20] Allow to set a multiplier on source, needed for LFO --- src/sfizz/Region.cpp | 14 +++++++------- src/sfizz/Region.h | 6 +++++- src/sfizz/Synth.cpp | 8 ++++---- src/sfizz/modulations/ModMatrix.cpp | 14 ++++++++------ src/sfizz/modulations/ModMatrix.h | 3 ++- tests/RegionTHelpers.cpp | 4 ++-- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 685d7587..25d38f40 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -940,9 +940,9 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con 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; + return x.source.id() == ModId::Controller && + x.source.parameters().cc == ccNumber && + x.target == target; }); Connection *conn; @@ -951,12 +951,12 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con else { connections.emplace_back(); conn = &connections.back(); - conn->first = ModKey::createCC(ccNumber, 0, 0, 0, 0); - conn->second = target; + conn->source = ModKey::createCC(ccNumber, 0, 0, 0, 0); + conn->target = target; } // - ModKey::Parameters p = conn->first.parameters(); + ModKey::Parameters p = conn->source.parameters(); switch (opcode.category) { case kOpcodeOnCcN: setValueFromOpcode(opcode, p.value, range); @@ -977,7 +977,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con assert(false); break; } - conn->first = ModKey(ModId::Controller, {}, p); + conn->source = ModKey(ModId::Controller, {}, p); } return true; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 0426d6d7..f226a331 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -373,7 +373,11 @@ struct Region { bool triggerOnNote { true }; // Modulation matrix connections - typedef std::pair Connection; + struct Connection { + ModKey source; + ModKey target; + float sourceDepth = 1.0f; + }; std::vector connections; // Parent diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 61fb532d..08d5f5e5 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1357,7 +1357,7 @@ void sfz::Synth::setupModMatrix() for (const Region::Connection& conn : region->connections) { ModGenerator* gen = nullptr; - switch (conn.first.id()) { + switch (conn.source.id()) { case ModId::Controller: gen = genController.get(); break; @@ -1370,8 +1370,8 @@ void sfz::Synth::setupModMatrix() if (!gen) continue; - ModMatrix::SourceId source = mm.registerSource(conn.first, *gen); - ModMatrix::TargetId target = mm.registerTarget(conn.second); + ModMatrix::SourceId source = mm.registerSource(conn.source, *gen); + ModMatrix::TargetId target = mm.registerTarget(conn.target); ASSERT(source); if (!source) { @@ -1385,7 +1385,7 @@ void sfz::Synth::setupModMatrix() continue; } - if (!mm.connect(source, target)) { + if (!mm.connect(source, target, conn.sourceDepth)) { DBG("[sfizz] Failed to connect modulation source and target"); ASSERTFALSE; } diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index c4893fd0..ef68215e 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -34,7 +34,7 @@ struct ModMatrix::Impl { }; struct ConnectionData { - // nothing + float sourceDepth_ {}; }; struct Target { @@ -176,7 +176,7 @@ ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) return TargetId(it->second); } -bool ModMatrix::connect(SourceId sourceId, TargetId targetId) +bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth) { Impl& impl = *impl_; unsigned sourceIndex = sourceId.number(); @@ -186,7 +186,8 @@ bool ModMatrix::connect(SourceId sourceId, TargetId targetId) return false; Impl::Target& target = impl.targets_[targetIndex]; - /*Impl::ConnectionData& conn =*/ target.connectedSources[sourceIndex]; + Impl::ConnectionData& conn = target.connectedSources[sourceIndex]; + conn.sourceDepth_ = sourceDepth; return true; } @@ -317,6 +318,7 @@ float* ModMatrix::getModulation(TargetId targetId) // 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 @@ -334,16 +336,16 @@ float* ModMatrix::getModulation(TargetId targetId) source.gen->generate(source.key, impl.voiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= temp[i]; + buffer[i] *= sourceDepth * temp[i]; } else if (targetFlags & kModIsPercentMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= 0.01f * temp[i]; + buffer[i] *= (0.01f * sourceDepth) * temp[i]; } else { ASSERT(targetFlags & kModIsAdditive); for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += temp[i]; + buffer[i] += sourceDepth * temp[i]; } } } diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 4622f4b1..f9338d68 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -90,9 +90,10 @@ public: * * @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); + bool connect(SourceId sourceId, TargetId targetId, float sourceDepth); /** * @brief Reinitialize modulation sources overall. diff --git a/tests/RegionTHelpers.cpp b/tests/RegionTHelpers.cpp index d55f05ad..a47b56ed 100644 --- a/tests/RegionTHelpers.cpp +++ b/tests/RegionTHelpers.cpp @@ -27,7 +27,7 @@ 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.first.parameters(); + const sfz::ModKey::Parameters p = conn.source.parameters(); if (p.cc == cc) return p; } @@ -37,5 +37,5 @@ sfz::ModKey::Parameters RegionCCView::at(int cc) const bool RegionCCView::match(const sfz::Region::Connection& conn) const { - return conn.first.id() == sfz::ModId::Controller && conn.second == target_; + return conn.source.id() == sfz::ModId::Controller && conn.target == target_; } From 30788baec410318bca7042a748819690ebbbbeee Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 14:02:25 +0200 Subject: [PATCH 12/20] Add note on NXYZ indices [ci skip] --- src/sfizz/modulations/ModKey.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 48977537..37b8b072 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -57,6 +57,8 @@ public: //! 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 }; }; From 748b8dc22ce143209d2146ed6e7f76b079b184e7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 15:47:43 +0200 Subject: [PATCH 13/20] Don't forget to apply source depth on first source --- src/sfizz/modulations/ModMatrix.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index ef68215e..b3f5fe13 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -329,6 +329,10 @@ float* ModMatrix::getModulation(TargetId targetId) if (useThisSource) { if (isFirstSource) { source.gen->generate(source.key, impl.voiceId_, buffer); + if (sourceDepth != 1) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= sourceDepth; + } isFirstSource = false; } else { From 0e35f392a764e9ab001daca93c4bf7ccc0adde72 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 22:51:44 +0200 Subject: [PATCH 14/20] Fix a mistake in toString for CC --- src/sfizz/modulations/ModKey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 69d8750f..5ba5e02c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -85,7 +85,7 @@ std::string ModKey::toString() const case ModId::Controller: return absl::StrCat("Controller ", params_.cc, " {curve=", params_.curve, ", smooth=", params_.smooth, - ", value=", params_.value, ", step=", params_.value, "}"); + ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: return absl::StrCat("EG ", 1 + params_.N); case ModId::LFO: From 6c870a53d3ee1937217e93692b3455978fb1cf19 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 22:59:59 +0200 Subject: [PATCH 15/20] Add test --- src/sfizz/modulations/ModMatrix.cpp | 42 +++++++++++++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 5 ++++ tests/ModulationsT.cpp | 22 +++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index b3f5fe13..48b61f25 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -13,6 +13,7 @@ #include "SIMDHelpers.h" #include "Debug.h" #include +#include #include #include @@ -382,4 +383,45 @@ 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 index f9338d68..516052ae 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -167,6 +167,11 @@ public: */ 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_; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 3db0a96a..b3f6776c 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -6,6 +6,7 @@ #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" +#include "sfizz/Synth.h" #include "catch2/catch.hpp" TEST_CASE("[Modulations] Identifiers") @@ -74,3 +75,24 @@ TEST_CASE("[Modulations] Display names") 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" +} +)"); +} From 3a665e503c25e89c9c415db3755283fe41df6455 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 30 Jul 2020 01:20:35 +0200 Subject: [PATCH 16/20] Fix build trouble on MSVC --- src/sfizz/modulations/ModMatrix.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 516052ae..fa1f2633 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -6,6 +6,7 @@ #pragma once #include "../NumericId.h" +#include #include #include From 42c4d964dbfd8dbe32bd9eecdf276fadbd2f0ee0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 30 Jul 2020 13:48:07 +0200 Subject: [PATCH 17/20] Fix small mistakes --- src/sfizz/Synth.cpp | 2 +- tests/ModulationsT.cpp | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 08d5f5e5..71f9f500 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1380,7 +1380,7 @@ void sfz::Synth::setupModMatrix() } ASSERT(target); - if (!source) { + if (!target) { DBG("[sfizz] Failed to register modulation target"); continue; } diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index b3f6776c..709ec36d 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -34,20 +34,22 @@ TEST_CASE("[Modulations] Flags") static auto* checkBasicFlags = +[](int flags) { REQUIRE(flags != sfz::kModFlagsInvalid); - REQUIRE(((flags & sfz::kModIsPerCycle) ^ - (flags & sfz::kModIsPerVoice)) != 0); + REQUIRE((bool(flags & sfz::kModIsPerCycle) + + bool(flags & sfz::kModIsPerVoice)) == 1); }; static auto* checkSourceFlags = +[](int flags) { checkBasicFlags(flags); - // nothing else + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 0); }; static auto* checkTargetFlags = +[](int flags) { checkBasicFlags(flags); - REQUIRE(((flags & sfz::kModIsAdditive) ^ - (flags & sfz::kModIsMultiplicative) ^ - (flags & sfz::kModIsPercentMultiplicative)) != 0); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 1); }; sfz::ModIds::forEachSourceId([](sfz::ModId id) From acac06aae8e21bc36bc0a6c33718b756b6201041 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 16:26:04 +0200 Subject: [PATCH 18/20] Rename v/r identifier vars to designate them as current --- src/sfizz/modulations/ModMatrix.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 48b61f25..fe5866be 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -24,8 +24,8 @@ struct ModMatrix::Impl { uint32_t samplesPerBlock_ {}; uint32_t numFrames_ {}; - NumericId voiceId_ {}; - NumericId regionId_ {}; + NumericId currentVoiceId_ {}; + NumericId currentRegionId_ {}; struct Source { ModKey key; @@ -249,8 +249,8 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) { Impl& impl = *impl_; - impl.voiceId_ = voiceId; - impl.regionId_ = regionId; + impl.currentVoiceId_ = voiceId; + impl.currentRegionId_ = regionId; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); @@ -268,8 +268,8 @@ void ModMatrix::endVoice() { Impl& impl = *impl_; const uint32_t numFrames = impl.numFrames_; - const NumericId voiceId = impl.voiceId_; - const NumericId regionId = impl.regionId_; + const NumericId voiceId = impl.currentVoiceId_; + const NumericId regionId = impl.currentRegionId_; for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { @@ -281,8 +281,8 @@ void ModMatrix::endVoice() } } - impl.voiceId_ = {}; - impl.regionId_ = {}; + impl.currentVoiceId_ = {}; + impl.currentRegionId_ = {}; } float* ModMatrix::getModulation(TargetId targetId) @@ -291,7 +291,7 @@ float* ModMatrix::getModulation(TargetId targetId) return nullptr; Impl& impl = *impl_; - const NumericId regionId = impl.regionId_; + const NumericId regionId = impl.currentRegionId_; const uint32_t targetIndex = targetId.number(); Impl::Target &target = impl.targets_[targetIndex]; const int targetFlags = target.key.flags(); @@ -329,7 +329,7 @@ float* ModMatrix::getModulation(TargetId targetId) if (useThisSource) { if (isFirstSource) { - source.gen->generate(source.key, impl.voiceId_, buffer); + source.gen->generate(source.key, impl.currentVoiceId_, buffer); if (sourceDepth != 1) { for (uint32_t i = 0; i < numFrames; ++i) buffer[i] *= sourceDepth; @@ -338,7 +338,7 @@ float* ModMatrix::getModulation(TargetId targetId) } else { absl::Span temp(impl.temp_.data(), numFrames); - source.gen->generate(source.key, impl.voiceId_, temp); + source.gen->generate(source.key, impl.currentVoiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) buffer[i] *= sourceDepth * temp[i]; From fb873ad895444a03b7ae4c1bdadb0b0690b7db1e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 16:28:54 +0200 Subject: [PATCH 19/20] Do not require mod generators to override a couple of methods --- src/sfizz/modulations/ModGenerator.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index 457460c3..2229ec15 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -24,12 +24,12 @@ public: /** * @brief Set the sample rate */ - virtual void setSampleRate(double sampleRate) = 0; + virtual void setSampleRate(double sampleRate) { (void)sampleRate; } /** * @brief Set the maximum block size */ - virtual void setSamplesPerBlock(unsigned count) = 0; + virtual void setSamplesPerBlock(unsigned count) { (void)count; } /** * @brief Initialize the generator. From 094107206a33728c3ab53ae5638eb7adcdfecfec Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 17:31:30 +0200 Subject: [PATCH 20/20] Ensure to generate sources once only --- src/sfizz/modulations/ModMatrix.cpp | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index fe5866be..41b1c844 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -51,8 +51,6 @@ struct ModMatrix::Impl { std::vector sources_; std::vector targets_; - - Buffer temp_; }; ModMatrix::ModMatrix() @@ -104,8 +102,6 @@ void ModMatrix::setSamplesPerBlock(unsigned samplesPerBlock) } for (Impl::Target &target : impl.targets_) target.buffer.resize(samplesPerBlock); - - impl.temp_.resize(samplesPerBlock); } ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& gen) @@ -315,7 +311,7 @@ float* ModMatrix::getModulation(TargetId targetId) auto sourcesEnd = target.connectedSources.end(); bool isFirstSource = true; - // generate first source in output buffer, next sources in temporary buffer + // generate sources in their dedicated buffers // then add or multiply, depending on target flags while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; @@ -328,29 +324,37 @@ float* ModMatrix::getModulation(TargetId targetId) 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) { - source.gen->generate(source.key, impl.currentVoiceId_, buffer); if (sourceDepth != 1) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= sourceDepth; + buffer[i] = sourceDepth * sourceBuffer[i]; + } + else { + copy(absl::Span(sourceBuffer), buffer); } isFirstSource = false; } else { - absl::Span temp(impl.temp_.data(), numFrames); - source.gen->generate(source.key, impl.currentVoiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= sourceDepth * temp[i]; + buffer[i] *= sourceDepth * sourceBuffer[i]; } else if (targetFlags & kModIsPercentMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= (0.01f * sourceDepth) * temp[i]; + buffer[i] *= (0.01f * sourceDepth) * sourceBuffer[i]; } else { ASSERT(targetFlags & kModIsAdditive); for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += sourceDepth * temp[i]; + buffer[i] += sourceDepth * sourceBuffer[i]; } } } @@ -361,12 +365,12 @@ float* ModMatrix::getModulation(TargetId targetId) // if there were no source, fill output with the neutral element if (isFirstSource) { if (targetFlags & kModIsMultiplicative) - sfz::fill(buffer, 1.0f); + fill(buffer, 1.0f); else if (targetFlags & kModIsPercentMultiplicative) - sfz::fill(buffer, 100.0f); + fill(buffer, 100.0f); else { ASSERT(targetFlags & kModIsAdditive); - sfz::fill(buffer, 0.0f); + fill(buffer, 0.0f); } }