From eab53ba05ab95d8e29aabbe2c0be54edcf4da1a6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 09:39:05 +0200 Subject: [PATCH] Add SFZv2 LFO and a small set of modulation targets --- dpf.mk | 4 + src/CMakeLists.txt | 6 + src/sfizz/Config.h | 2 + src/sfizz/Defaults.h | 16 ++ src/sfizz/LFO.cpp | 295 ++++++++++++++++++++++++++ src/sfizz/LFO.h | 112 ++++++++++ src/sfizz/LFODescription.cpp | 31 +++ src/sfizz/LFODescription.h | 48 +++++ src/sfizz/Region.cpp | 241 +++++++++++++++++++++ src/sfizz/Region.h | 5 + src/sfizz/Synth.cpp | 9 + src/sfizz/Synth.h | 3 + src/sfizz/Voice.cpp | 19 ++ src/sfizz/Voice.h | 19 +- src/sfizz/modulations/sources/LFO.cpp | 67 ++++++ src/sfizz/modulations/sources/LFO.h | 23 ++ 16 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 src/sfizz/LFO.cpp create mode 100644 src/sfizz/LFO.h create mode 100644 src/sfizz/LFODescription.cpp create mode 100644 src/sfizz/LFODescription.h create mode 100644 src/sfizz/modulations/sources/LFO.cpp create mode 100644 src/sfizz/modulations/sources/LFO.h diff --git a/dpf.mk b/dpf.mk index 4060db8d..ec3038fd 100644 --- a/dpf.mk +++ b/dpf.mk @@ -1,3 +1,4 @@ + # # A build file to help using sfizz with the DISTRHO Plugin Framework (DPF) # ------------------------------------------------------------------------ @@ -65,6 +66,7 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/Controller.cpp \ + src/sfizz/modulations/sources/LFO.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ @@ -91,6 +93,8 @@ SFIZZ_SOURCES = \ src/sfizz/FilterPool.cpp \ src/sfizz/FloatEnvelopes.cpp \ src/sfizz/Logger.cpp \ + src/sfizz/LFO.cpp \ + src/sfizz/LFODescription.cpp \ src/sfizz/MidiState.cpp \ src/sfizz/OpcodeCleanup.cpp \ src/sfizz/Opcode.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1199157..63cdd25f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set (SFIZZ_HEADERS sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h sfizz/modulations/sources/Controller.h + sfizz/modulations/sources/LFO.h sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h @@ -70,6 +71,8 @@ set (SFIZZ_HEADERS sfizz/Interpolators.h sfizz/Interpolators.hpp sfizz/Logger.h + sfizz/LFO.h + sfizz/LFODescription.h sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h @@ -133,11 +136,14 @@ set (SFIZZ_SOURCES sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp + sfizz/LFO.cpp + sfizz/LFODescription.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp + sfizz/modulations/sources/LFO.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 03f7f378..d3592bc5 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -64,6 +64,8 @@ namespace config { constexpr unsigned int defaultAlignment { 16 }; constexpr int filtersInPool { maxVoices * 2 }; constexpr int excessFileFrames { 8 }; + constexpr int maxLFOSubs { 8 }; + constexpr int maxLFOSteps { 128 }; /** * @brief The threshold for age stealing. * In percentage of the voice's max age. diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 8be2fc96..c30d4192 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -207,6 +207,22 @@ namespace Default constexpr int bendStep { 1 }; constexpr uint8_t bendSmooth { 0 }; + // Modulation: LFO + constexpr int numLFOs { 4 }; + constexpr int numLFOSubs { 2 }; + constexpr int numLFOSteps { 8 }; + constexpr Range lfoFreqRange { 0.0, 100.0 }; + constexpr Range lfoPhaseRange { 0.0, 360.0 }; + constexpr Range lfoDelayRange { 0.0, 30.0 }; + constexpr Range lfoFadeRange { 0.0, 30.0 }; + constexpr Range lfoCountRange { 0, 1000 }; + constexpr Range lfoStepsRange { 0, static_cast(config::maxLFOSteps) }; + constexpr Range lfoStepXRange { -100.0, 100.0 }; + constexpr Range lfoWaveRange { 0, 15 }; + constexpr Range lfoOffsetRange { -1.0, 1.0 }; + constexpr Range lfoRatioRange { 0.0, 100.0 }; + constexpr Range lfoScaleRange { 0.0, 1.0 }; + // Envelope generators constexpr float attack { 0 }; constexpr float decay { 0 }; diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp new file mode 100644 index 00000000..6d1da07d --- /dev/null +++ b/src/sfizz/LFO.cpp @@ -0,0 +1,295 @@ +// 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 "LFO.h" +#include "LFODescription.h" +#include "MathHelpers.h" +#include "SIMDHelpers.h" +#include "Config.h" +#include +#include +#include + +namespace sfz { + +struct LFO::Impl { + float sampleRate_ = 0; + + // control + const LFODescription* desc_ = nullptr; + + // state + size_t delayFramesLeft_ = 0; + float fadeInPole_ = 0; + float fadeInMemory_ = 0; + std::array subPhases_ {{}}; + std::array sampleHoldMem_ {{}}; +}; + +LFO::LFO() + : impl_(new Impl) +{ + impl_->sampleRate_ = config::defaultSampleRate; + impl_->desc_ = &LFODescription::getDefault(); +} + +LFO::~LFO() +{ +} + +void LFO::setSampleRate(double sampleRate) +{ + impl_->sampleRate_ = sampleRate; +} + +void LFO::configure(const LFODescription* desc) +{ + impl_->desc_ = desc ? desc : &LFODescription::getDefault(); +} + +void LFO::start() +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const float sampleRate = impl.sampleRate_; + + impl.subPhases_.fill(desc.phase0); + impl.sampleHoldMem_.fill(0.0f); + + const float delay = desc.delay; + impl.delayFramesLeft_ = (delay > 0) ? static_cast(std::ceil(sampleRate * delay)) : 0u; + + const float fade = desc.fade; + impl.fadeInPole_ = (fade > 0) ? std::exp(-1.0 / (fade * sampleRate)) : 0.0f; + impl.fadeInMemory_ = 0; +} + +template <> +inline float LFO::eval(float phase) +{ + float y = -4 * phase + 2; + y = (phase < 0.25f) ? (4 * phase) : y; + y = (phase > 0.75f) ? (4 * phase - 4) : y; + return y; +} + +template <> +inline float LFO::eval(float phase) +{ + float x = phase + phase - 1; + return 4 * x * (1 - std::fabs(x)); +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.75f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.5f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.25f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.125f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return 2 * phase - 1; +} + +template <> +inline float LFO::eval(float phase) +{ + return 1 - 2 * phase; +} + +template +void LFO::processWave(unsigned nth, absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + + for (size_t i = 0; i < numFrames; ++i) { + out[i] += offset + scale * eval(phase); + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + } + + impl.subPhases_[nth] = phase; +} + +template +void LFO::processSH(unsigned nth, absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + float sampleHoldValue = impl.sampleHoldMem_[nth]; + + for (size_t i = 0; i < numFrames; ++i) { + out[i] += offset + scale * sampleHoldValue; + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + + // value updates twice every period + bool updateValue = (int)(phase * 2.0) != (int)((phase + incrPhase) * 2.0); + + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + + if (updateValue) { + std::uniform_real_distribution dist(-1.0f, +1.0f); + sampleHoldValue = dist(Random::randomGenerator); + } + } + + impl.subPhases_[nth] = phase; + impl.sampleHoldMem_[nth] = sampleHoldValue; +} + +void LFO::processSteps(absl::Span out) +{ + unsigned nth = 0; + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + const LFODescription::StepSequence& seq = *desc.seq; + const float* steps = seq.steps.data(); + unsigned numSteps = seq.steps.size(); + + if (numSteps <= 0) + return; + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + + for (size_t i = 0; i < numFrames; ++i) { + float step = steps[static_cast(phase * numSteps)]; + out[i] += offset + scale * step; + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + } + + impl.subPhases_[nth] = phase; +} + +void LFO::process(absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + size_t numFrames = out.size(); + + fill(out, 0.0f); + + size_t skipFrames = std::min(numFrames, impl.delayFramesLeft_); + if (skipFrames > 0) { + impl.delayFramesLeft_ -= skipFrames; + out.remove_prefix(skipFrames); + numFrames -= skipFrames; + } + + unsigned subno = 0; + const unsigned countSubs = desc.sub.size(); + + if (countSubs < 1) + return; + + if (desc.seq) { + processSteps(out); + ++subno; + } + + for (; subno < countSubs; ++subno) { + switch (desc.sub[subno].wave) { + case LFOWave::Triangle: + processWave(subno, out); + break; + case LFOWave::Sine: + processWave(subno, out); + break; + case LFOWave::Pulse75: + processWave(subno, out); + break; + case LFOWave::Square: + processWave(subno, out); + break; + case LFOWave::Pulse25: + processWave(subno, out); + break; + case LFOWave::Pulse12_5: + processWave(subno, out); + break; + case LFOWave::Ramp: + processWave(subno, out); + break; + case LFOWave::Saw: + processWave(subno, out); + break; + case LFOWave::RandomSH: + processSH(subno, out); + break; + } + } + + float fadeIn = impl.fadeInMemory_; + const float fadeInPole = impl.fadeInPole_; + for (size_t i = 0; i < numFrames; ++i) { + out[i] *= fadeIn; + fadeIn = fadeInPole * fadeIn + (1 - fadeInPole); + } + impl.fadeInMemory_ = fadeIn; +} + +} // namespace sfz diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h new file mode 100644 index 00000000..86d1f0f3 --- /dev/null +++ b/src/sfizz/LFO.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { + +enum class LFOWave : int; +struct LFODescription; + +/* + * General + + lfoN_freq: Base frequency - Allow modulations at A-rate + lfoN_phase: Initial phase + lfoN_delay: Delay + lfoN_fade: Time to fade-in + lfoN_count: Number of repetitions - not implemented in ARIA + lfoN_steps: Length of the step sequence - 1 to 128 + lfoN_steps_onccX: ??? TODO(jpc) seen in Rapture + lfoN_stepX: Value of the Xth step of the sequence - -100% to +100% + lfoN_stepX_onccY: ??? TODO(jpc) check this. override/modulate step in sequence? + + note: LFO evaluates between -1 to +1 + + note: make the step sequencer override the main wave when present. + subwaves are ARIA, step sequencer is Cakewalk, so do our own thing + which makes the most sense. + + * Subwaveforms + X: - #1/omitted: the main wave + - #2-#8: a subwave + + note: if there are gaps in subwaveforms, these subwaveforms which are gaps + will be initialized and processed. + + example: lfo1_ratio4=1.0 // instanciate implicitly the subs #2 and #3 + + lfoN_wave[X]: Wave + lfoN_offset[X]: DC offset - Add to LFO output; not affected by scale. + lfoN_ratio[X]: Sub ratio - Frequency = (Ratio * Base Frequency) + lfoN_scale[X]: Sub scale - Amplitude of sub +*/ + +class LFO { +public: + LFO(); + ~LFO(); + + /** + Sets the sample rate. + */ + void setSampleRate(double sampleRate); + + /** + Attach some control parameters to this LFO. + The control structure is owned by the caller. + */ + void configure(const LFODescription* desc); + + /** + Start processing a LFO as a region is triggered. + Prepares the delay, phases, fade-in, etc.. + */ + void start(); + + /** + Process a cycle of the oscillator. + + TODO(jpc) frequency modulations + */ + void process(absl::Span out); + +private: + /** + Evaluate the wave at a given phase. + Phase must be in the range 0 to 1 excluded. + */ + template + static float eval(float phase); + + /** + Process the nth subwaveform, adding to the buffer. + + This definition is duplicated per each wave, a strategy to avoid a switch + on wave type inside the frame loop. + */ + template + void processWave(unsigned nth, absl::Span out); + + /** + Process a sample-and-hold subwaveform, adding to the buffer. + */ + template + void processSH(unsigned nth, absl::Span out); + + /** + Process the step sequencer, adding to the buffer. + */ + void processSteps(absl::Span out); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/LFODescription.cpp b/src/sfizz/LFODescription.cpp new file mode 100644 index 00000000..4e846ef2 --- /dev/null +++ b/src/sfizz/LFODescription.cpp @@ -0,0 +1,31 @@ +// 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 "LFODescription.h" + +namespace sfz { + +LFODescription::LFODescription() +{ + sub.resize(1); +} + +LFODescription::~LFODescription() +{ +} + +const LFODescription& LFODescription::getDefault() +{ + static LFODescription desc = []() -> LFODescription + { + LFODescription desc; + desc.sub.resize(1); + return desc; + }(); + return desc; +} + +} // namespace sfz diff --git a/src/sfizz/LFODescription.h b/src/sfizz/LFODescription.h new file mode 100644 index 00000000..8f5344c9 --- /dev/null +++ b/src/sfizz/LFODescription.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { + +enum class LFOWave : int { + Triangle, + Sine, + Pulse75, + Square, + Pulse25, + Pulse12_5, + Ramp, + Saw, + // ARIA extra + RandomSH = 12, +}; + +struct LFODescription { + LFODescription(); + ~LFODescription(); + static const LFODescription& getDefault(); + float freq = 0; // lfoN_freq + float phase0 = 0; // lfoN_phase + float delay = 0; // lfoN_delay + float fade = 0; // lfoN_fade + unsigned count = 0; // lfoN_count + struct Sub { + LFOWave wave = LFOWave::Triangle; // lfoN_wave[X] + float offset = 0; // lfoN_offset[X] + float ratio = 1; // lfoN_ratio[X] + float scale = 1; // lfoN_scale[X] + }; + struct StepSequence { + std::vector steps {}; // lfoN_stepX - normalized to unity + }; + absl::optional seq; + std::vector sub; +}; + +} // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 25d38f40..95619178 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -786,6 +786,228 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, bendSmooth, Default::smoothCCRange); break; + // Modulation: LFO + case hash("lfo&_freq"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].freq, Default::lfoFreqRange); + } + break; + case hash("lfo&_phase"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoPhaseRange)) { + float normalPhase = *value * (1.0 / 360.0); + normalPhase -= int(normalPhase); + normalPhase += (normalPhase < 0) ? 1 : 0; + lfos[lfoNumber - 1].phase0 = normalPhase; + } + } + break; + case hash("lfo&_delay"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].delay, Default::lfoDelayRange); + } + break; + case hash("lfo&_fade"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].fade, Default::lfoFadeRange); + } + break; + case hash("lfo&_count"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].count, Default::lfoCountRange); + } + break; + case hash("lfo&_steps"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoStepsRange)) { + if (!lfos[lfoNumber - 1].seq) + lfos[lfoNumber - 1].seq = LFODescription::StepSequence(); + lfos[lfoNumber - 1].seq->steps.resize(*value); + } + } + break; + case hash("lfo&_step&"): + { + const auto lfoNumber = opcode.parameters.front(); + const auto stepNumber = opcode.parameters[1]; + if (lfoNumber == 0 || stepNumber == 0 || stepNumber > config::maxLFOSteps) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoStepXRange)) { + if (!lfos[lfoNumber - 1].seq) + lfos[lfoNumber - 1].seq = LFODescription::StepSequence(); + if (!extendIfNecessary(lfos[lfoNumber - 1].seq->steps, stepNumber, Default::numLFOSteps)) + return false; + lfos[lfoNumber - 1].seq->steps[stepNumber - 1] = *value * 0.01f; + } + } + break; + case hash("lfo&_wave&"): // also lfo&_wave + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoWaveRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].wave = static_cast(*value); + } + } + break; + case hash("lfo&_offset&"): // also lfo&_offset + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoOffsetRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].offset = *value; + } + } + break; + case hash("lfo&_ratio&"): // also lfo&_ratio + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoRatioRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].ratio = *value; + } + } + break; + case hash("lfo&_scale&"): // also lfo&_scale + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoScaleRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].scale = *value; + } + } + break; + + // Modulation: LFO (targets) + case hash("lfo&_amplitude"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_pan"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::panCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Pan, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_width"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::widthCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Width, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_position"): // sfizz extension + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::positionCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Position, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_pitch"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Pitch, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_volume"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Volume, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + // Amplitude Envelope case hash("ampeg_attack"): setValueFromOpcode(opcode, amplitudeEG.attack, Default::egTimeRange); @@ -1324,3 +1546,22 @@ float sfz::Region::getBendInCents(float bend) const noexcept { return bend > 0.0f ? bend * static_cast(bendUp) : -bend * static_cast(bendDown); } + +sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +{ + auto pred = [&source, &target](const Connection& c) + { + return c.source == source && c.target == target; + }; + + auto it = std::find_if(connections.begin(), connections.end(), pred); + if (it != connections.end()) + return *it; + + sfz::Region::Connection c; + c.source = source; + c.target = target; + + connections.push_back(c); + return connections.back(); +} diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index f226a331..a4c4f291 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -12,6 +12,7 @@ #include "EGDescription.h" #include "EQDescription.h" #include "FilterDescription.h" +#include "LFODescription.h" #include "Opcode.h" #include "AudioBuffer.h" #include "MidiState.h" @@ -364,6 +365,9 @@ struct Region { EGDescription pitchEG; EGDescription filterEG; + // LFOs + std::vector lfos; + bool hasStereoSample { false }; // Effects @@ -379,6 +383,7 @@ struct Region { float sourceDepth = 1.0f; }; std::vector connections; + Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); // Parent RegionSet* parent { nullptr }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 71f9f500..8a268b64 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -16,6 +16,7 @@ #include "modulations/ModKey.h" #include "modulations/ModId.h" #include "modulations/sources/Controller.h" +#include "modulations/sources/LFO.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -42,6 +43,7 @@ sfz::Synth::Synth(int numVoices) // modulation sources genController.reset(new ControllerSource(resources)); + genLFO.reset(new LFOSource(*this)); } sfz::Synth::~Synth() @@ -452,6 +454,7 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; + size_t maxLFOs { 0 }; while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -562,6 +565,7 @@ void sfz::Synth::finalizeSfzLoad() region->registerTempo(2.0f); maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); + maxLFOs = max(maxLFOs, region->lfos.size()); ++currentRegionIndex; } @@ -571,6 +575,7 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; + settingsPerVoice.maxLFOs = maxLFOs; applySettingsPerVoice(); @@ -1346,6 +1351,7 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); + voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); } } @@ -1361,6 +1367,9 @@ void sfz::Synth::setupModMatrix() case ModId::Controller: gen = genController.get(); break; + case ModId::LFO: + gen = genLFO.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 8133b676..5b49ddd1 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -27,6 +27,7 @@ namespace sfz { class ControllerSource; +class LFOSource; /** * @brief This class is the core of the sfizz library. In C++ it is the main point @@ -767,11 +768,13 @@ private: // Modulation source generators std::unique_ptr genController; + std::unique_ptr genLFO; // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; + size_t maxLFOs { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 97fa044d..03194ece 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -12,6 +12,7 @@ #include "SIMDHelpers.h" #include "Panning.h" #include "SfzHelpers.h" +#include "LFO.h" #include "modulations/ModId.h" #include "modulations/ModKey.h" #include "modulations/ModMatrix.h" @@ -34,6 +35,10 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); } +sfz::Voice::~Voice() +{ +} + void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept { ASSERT(value >= 0.0f && value <= 1.0f); @@ -235,6 +240,9 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); + + for (auto& lfo : lfos) + lfo->setSampleRate(sampleRate); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept @@ -773,6 +781,17 @@ void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) equalizers.reserve(numFilters); } +void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs) +{ + lfos.resize(numLFOs); + + for (size_t i = 0; i < numLFOs; ++i) { + auto lfo = absl::make_unique(); + lfo->setSampleRate(sampleRate); + lfos[i] = std::move(lfo); + } +} + void sfz::Voice::setupOscillatorUnison() { int m = region->oscillatorMulti; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 52a7f0c8..17913bb3 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -22,6 +22,7 @@ namespace sfz { enum InterpolatorModel : int; +class LFO; /** * @brief The SFZ voice are the polyphony holders. They get activated by the synth * and tasked to play a given region until the end, stopping on note-offs, off-groups @@ -38,6 +39,9 @@ public: * @param midiState */ Voice(int voiceNumber, Resources& resources); + + ~Voice(); + enum class TriggerType { NoteOn, NoteOff, @@ -270,6 +274,12 @@ public: * @return */ const Region* getRegion() const noexcept { return region; } + /** + * @brief Get the LFO designated by the given index + * + * @param index + */ + LFO* getLFO(size_t index) { return lfos[index].get(); } /** * @brief Set the max number of filters per voice * @@ -279,9 +289,15 @@ public: /** * @brief Set the max number of EQs per voice * - * @param numFilters + * @param numEQs */ void setMaxEQsPerVoice(size_t numEQs); + /** + * @brief Set the max number of LFOs per voice + * + * @param numLFOs + */ + void setMaxLFOsPerVoice(size_t numLFOs); /** * @brief Release the voice after a given delay * @@ -433,6 +449,7 @@ private: std::vector filters; std::vector equalizers; + std::vector> lfos; ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; diff --git a/src/sfizz/modulations/sources/LFO.cpp b/src/sfizz/modulations/sources/LFO.cpp new file mode 100644 index 00000000..0f604898 --- /dev/null +++ b/src/sfizz/modulations/sources/LFO.cpp @@ -0,0 +1,67 @@ +// 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 "LFO.h" +#include "../../LFO.h" +#include "../../Synth.h" +#include "../../Voice.h" +#include "../../SIMDHelpers.h" +#include "../../Config.h" +#include "../../Debug.h" + +namespace sfz { + +LFOSource::LFOSource(Synth &synth) + : synth_(&synth) +{ +} + +void LFOSource::init(const ModKey& sourceKey, NumericId voiceId) +{ + Synth& synth = *synth_; + unsigned lfoIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (lfoIndex >= region->lfos.size()) { + ASSERTFALSE; + return; + } + + LFO* lfo = voice->getLFO(lfoIndex); + lfo->configure(®ion->lfos[lfoIndex]); + lfo->start(); +} + +void LFOSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + Synth& synth = *synth_; + const unsigned lfoIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + fill(buffer, 0.0f); + return; + } + + const Region* region = voice->getRegion(); + if (lfoIndex >= region->lfos.size()) { + ASSERTFALSE; + fill(buffer, 0.0f); + return; + } + + LFO* lfo = voice->getLFO(lfoIndex); + lfo->process(buffer); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/LFO.h b/src/sfizz/modulations/sources/LFO.h new file mode 100644 index 00000000..83a1e30f --- /dev/null +++ b/src/sfizz/modulations/sources/LFO.h @@ -0,0 +1,23 @@ +// 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" + +namespace sfz { +class Synth; + +class LFOSource : public ModGenerator { +public: + explicit LFOSource(Synth &synth); + void init(const ModKey& sourceKey, NumericId voiceId) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz