From 28c12f32f3d890844a69cbb77e2e49fa35567b02 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 26 Feb 2020 21:09:34 +0100 Subject: [PATCH] Initial effects --- src/CMakeLists.txt | 3 + src/sfizz/AudioSpan.h | 16 +++ src/sfizz/Config.h | 4 + src/sfizz/Effects.cpp | 137 ++++++++++++++++++++++ src/sfizz/Effects.h | 147 +++++++++++++++++++++++ src/sfizz/Region.cpp | 22 ++++ src/sfizz/Region.h | 13 +++ src/sfizz/Synth.cpp | 155 ++++++++++++++++++++++--- src/sfizz/Synth.h | 15 ++- src/sfizz/effects/Lofi.cpp | 213 ++++++++++++++++++++++++++++++++++ src/sfizz/effects/Lofi.h | 79 +++++++++++++ src/sfizz/effects/Nothing.cpp | 31 +++++ src/sfizz/effects/Nothing.h | 35 ++++++ 13 files changed, 852 insertions(+), 18 deletions(-) create mode 100644 src/sfizz/Effects.cpp create mode 100644 src/sfizz/Effects.h create mode 100644 src/sfizz/effects/Lofi.cpp create mode 100644 src/sfizz/effects/Lofi.h create mode 100644 src/sfizz/effects/Nothing.cpp create mode 100644 src/sfizz/effects/Nothing.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 57c71312..3580130c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,9 @@ set (SFIZZ_SOURCES sfizz/FloatEnvelopes.cpp sfizz/Logger.cpp sfizz/SfzFilter.cpp + sfizz/Effects.cpp + sfizz/effects/Nothing.cpp + sfizz/effects/Lofi.cpp ) include (SfizzSIMDSourceFiles) diff --git a/src/sfizz/AudioSpan.h b/src/sfizz/AudioSpan.h index 312c1e1b..c608848c 100644 --- a/src/sfizz/AudioSpan.h +++ b/src/sfizz/AudioSpan.h @@ -209,6 +209,22 @@ public: return {}; } + /** + * @brief Convert implicitly to a pointer of channels + */ + operator const float* const *() const noexcept + { + return spans.data(); + } + + /** + * @brief Convert implicitly to a pointer of channels + */ + operator float* const *() noexcept + { + return spans.data(); + } + /** * @brief Get a Span corresponding to a specific channel * diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 0b629ccd..b2a6f8a2 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -69,6 +69,10 @@ namespace config { */ const absl::string_view midnamManufacturer { "The Sfizz authors" }; const absl::string_view midnamModel { "Sfizz" }; + /** + Limit of how many "fxN" buses are accepted (in SFZv2, maximum is 4) + */ + constexpr int maxEffectBuses { 256 }; } // namespace config // Enable or disable SIMD accelerators by default diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp new file mode 100644 index 00000000..a5b3b964 --- /dev/null +++ b/src/sfizz/Effects.cpp @@ -0,0 +1,137 @@ +// 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 "Effects.h" +#include "AudioSpan.h" +#include "Opcode.h" +#include "SIMDHelpers.h" +#include "Config.h" +#include "effects/Nothing.h" +#include "effects/Lofi.h" +#include + +namespace sfz { + +void EffectFactory::registerStandardEffectTypes() +{ + // TODO + registerEffectType("lofi", fx::Lofi::makeInstance); +} + +void EffectFactory::registerEffectType(absl::string_view name, Effect::MakeInstance& make) +{ + FactoryEntry ent; + ent.name = std::string(name); + ent.make = &make; + _entries.push_back(std::move(ent)); +} + +Effect* EffectFactory::makeEffect(absl::Span members) +{ + const Opcode* opcode = nullptr; + + for (auto it = members.rbegin(); it != members.rend() && !opcode; ++it) { + if (it->lettersOnlyHash == hash("type")) + opcode = &*it; + } + + if (!opcode) { + DBG("The effect does not specify a type"); + return new sfz::fx::Nothing; + } + + absl::string_view type = opcode->value; + + auto it = _entries.begin(); + auto end = _entries.end(); + for (; it != end && it->name != type; ++it) + ; + + if (it == end) { + DBG("Unsupported effect type: " << type); + return new sfz::fx::Nothing; + } + + Effect* fx = it->make(members); + if (!fx) { + DBG("Could not instantiate effect of type: " << type); + return new sfz::fx::Nothing; + } + + return fx; +} + +/// +EffectBus::EffectBus() +{ +} + +EffectBus::~EffectBus() +{ +} + +void EffectBus::addEffect(std::unique_ptr fx) +{ + _effects.emplace_back(std::move(fx)); +} + +void EffectBus::clearInputs(unsigned nframes) +{ + AudioSpan(_inputs).first(nframes).fill(0.0f); + AudioSpan(_outputs).first(nframes).fill(0.0f); +} + +void EffectBus::addToInputs(const float* const addInput[], float addGain, unsigned nframes) +{ + if (addGain == 0) + return; + + for (unsigned c = 0; c < EffectChannels; ++c) { + absl::Span addIn(addInput[c], nframes); + sfz::multiplyAdd(addGain, addIn, _inputs.getSpan(c)); + } +} + +void EffectBus::init(double sampleRate) +{ + for (const auto& effectPtr : _effects) + effectPtr->init(sampleRate); +} + +void EffectBus::clear() +{ + for (const auto& effectPtr : _effects) + effectPtr->clear(); +} + +void EffectBus::process(unsigned nframes) +{ + size_t numEffects = _effects.size(); + + if (numEffects > 0 && hasNonZeroOutput()) { + _effects[0]->process( + AudioSpan(_inputs), AudioSpan(_outputs), nframes); + for (size_t i = 1; i < numEffects; ++i) + _effects[i]->process( + AudioSpan(_outputs), AudioSpan(_outputs), nframes); + } else + fx::Nothing().process( + AudioSpan(_inputs), AudioSpan(_outputs), nframes); +} + +void EffectBus::mixOutputsTo(float* const mainOutput[], float* const mixOutput[], unsigned nframes) +{ + const float gainToMain = _gainToMain; + const float gainToMix = _gainToMix; + + for (unsigned c = 0; c < EffectChannels; ++c) { + absl::Span fxOut = _outputs.getConstSpan(c); + sfz::multiplyAdd(gainToMain, fxOut, absl::Span(mainOutput[c], nframes)); + sfz::multiplyAdd(gainToMix, fxOut, absl::Span(mixOutput[c], nframes)); + } +} + +} // namespace sfz diff --git a/src/sfizz/Effects.h b/src/sfizz/Effects.h new file mode 100644 index 00000000..80462d0c --- /dev/null +++ b/src/sfizz/Effects.h @@ -0,0 +1,147 @@ +// 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 "AudioBuffer.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include +#include +#include + +namespace sfz { +struct Opcode; + +enum { + // Number of channels processed by effects + EffectChannels = 2, +}; + +/** + @brief Abstract base of SFZ effects + */ +class Effect { +public: + virtual ~Effect() {} + + /** + @brief Initializes with the given sample rate. + */ + virtual void init(double sampleRate) = 0; + + /** + @brief Reset the state to initial. + */ + virtual void clear() = 0; + + /** + @brief Computes a cycle of the effect in stereo. + */ + virtual void process(const float* const inputs[], float* const outputs[], unsigned nframes) = 0; + + /** + @brief Type of the factory function used to instantiate an effect given + the contents of the block + */ + typedef Effect* (MakeInstance)(absl::Span members); +}; + +/** + @brief SFZ effects factory + */ +class EffectFactory { +public: + /** + @brief Registers all available standard effects into the factory. + */ + void registerStandardEffectTypes(); + + /** + @brief Registers a user-defined effect into the factory. + */ + void registerEffectType(absl::string_view name, Effect::MakeInstance& make); + + /** + @brief Instantiates an effect given the contents of the block. + */ + Effect* makeEffect(absl::Span members); + +private: + struct FactoryEntry { + std::string name; + Effect::MakeInstance* make; + }; + + std::vector _entries; +}; + +/** + @brief Sequence of effects processed in series + */ +class EffectBus { +public: + EffectBus(); + ~EffectBus(); + + /** + @brief Adds an effect at the end of the bus. + */ + void addEffect(std::unique_ptr fx); + + /** + @brief Checks whether this bus can produce output. + */ + bool hasNonZeroOutput() const { return _gainToMain != 0 || _gainToMix != 0; } + + /** + @brief Sets the amount of effect output going to the main. + */ + void setGainToMain(float gain) { _gainToMain = gain; } + + /** + @brief Sets the amount of effect output going to the mix. + */ + void setGainToMix(float gain) { _gainToMix = gain; } + + /** + @brief Resets the input buffers to zero. + */ + void clearInputs(unsigned nframes); + + /** + @brief Adds some audio into the input buffer. + */ + void addToInputs(const float* const addInput[], float addGain, unsigned nframes); + + /** + @brief Initializes all effects in the bus with the given sample rate. + */ + void init(double sampleRate); + + /** + @brief Resets the state of all effects in the bus. + */ + void clear(); + + /** + @brief Computes a cycle of the effect bus. + */ + void process(unsigned nframes); + + /** + @brief Mixes the outputs into a pair of stereo signals: Main and Mix. + */ + void mixOutputsTo(float* const mainOutput[], float* const mixOutput[], unsigned nframes); + +private: + std::vector> _effects; + AudioBuffer _inputs { EffectChannels, config::defaultSamplesPerBlock }; + AudioBuffer _outputs { EffectChannels, config::defaultSamplesPerBlock }; + float _gainToMain = 0.0; + float _gainToMix = 0.0; +}; + +} // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e05708a0..35475263 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -733,6 +733,20 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange); break; + case hash("effect"): // effect& + { + const auto effectNumber = opcode.backParameter(); + if (!effectNumber || *effectNumber < 1 || *effectNumber > config::maxEffectBuses) + break; + auto value = readOpcode(opcode.value, {0, 100}); + if (!value) + break; + if (static_cast(*effectNumber + 1) > gainToEffect.size()) + gainToEffect.resize(*effectNumber + 1); + gainToEffect[*effectNumber] = *value / 100; + break; + } + // Ignored opcodes case hash("hichan"): case hash("lochan"): @@ -1073,3 +1087,11 @@ void sfz::Region::offsetAllKeys(int offset) noexcept crossfadeKeyOutRange.setEnd(offsetAndClamp(end, offset, Default::keyRange)); } } + +float sfz::Region::getGainToEffectBus(unsigned number) const noexcept +{ + if (number >= gainToEffect.size()) + return 0.0; + + return gainToEffect[number]; +} diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index d398a63e..cf08657b 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -39,6 +39,9 @@ struct Region { : midiState(midiState), defaultPath(std::move(defaultPath)) { ccSwitched.set(); + + gainToEffect.reserve(5); // sufficient room for main and fx1-4 + gainToEffect.push_back(1.0); // contribute 100% into the main bus } Region(const Region&) = default; ~Region() = default; @@ -206,6 +209,12 @@ struct Region { bool hasKeyswitches() const noexcept { return keyswitchDown || keyswitchUp || keyswitch || previousNote; } + /** + * @brief Get the gain this region contributes into the input of the Nth + * effect bus + */ + float getGainToEffectBus(unsigned number) const noexcept; + // Sound source: sample playback std::string sample {}; // Sample float delay { Default::delay }; // delay @@ -297,6 +306,10 @@ struct Region { EGDescription filterEG; bool isStereo { false }; + + // Effects + std::vector gainToEffect; + private: const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index d65baa2f..7ebbbb39 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -21,12 +21,16 @@ using namespace std::literals; sfz::Synth::Synth() + : Synth(config::numVoices) { - resetVoices(this->numVoices); } sfz::Synth::Synth(int numVoices) { + effectFactory.registerStandardEffectTypes(); + + effectBuses.reserve(5); // sufficient room for main and fx1-4 + resetVoices(numVoices); } @@ -70,7 +74,7 @@ void sfz::Synth::callback(absl::string_view header, const std::vector& m numCurves++; break; case hash("effect"): - // TODO: implement effects + handleEffectOpcodes(members); break; default: std::cerr << "Unknown header: " << header << '\n'; @@ -118,6 +122,10 @@ void sfz::Synth::clear() for (auto& list: ccActivationLists) list.clear(); regions.clear(); + effectBuses.clear(); + EffectBus* mainBus = new EffectBus; + effectBuses.emplace_back(mainBus); + mainBus->setGainToMain(1.0); resources.filePool.clear(); resources.logger.clear(); numGroups = 0; @@ -185,6 +193,75 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) } } +void sfz::Synth::handleEffectOpcodes(const std::vector& members) +{ + absl::string_view busName = "main"; + + auto getOrCreateBus = [this](unsigned index) -> EffectBus& { + if (index + 1 > effectBuses.size()) + effectBuses.resize(index + 1); + EffectBusPtr &slot = effectBuses[index]; + if (!slot) + slot.reset(new EffectBus); + return *slot; + }; + + for (const Opcode& opcode : members) { + switch (opcode.lettersOnlyHash) { + case hash("bus"): + busName = opcode.value; + break; + + // note(jpc): gain opcodes are linear volumes in % units + + case hash("directtomain"): + if (auto valueOpt = readOpcode(opcode.value, {0, 100})) + getOrCreateBus(0).setGainToMain(*valueOpt / 100); + break; + + case hash("fxtomain"): // fx&tomain + if (auto numberOpt = opcode.firstParameter()) { + unsigned number = *numberOpt; + if (number < 1 || number > config::maxEffectBuses) + break; + if (auto valueOpt = readOpcode(opcode.value, {0, 100})) + getOrCreateBus(number).setGainToMain(*valueOpt / 100); + } + break; + + case hash("fxtomix"): // fx&tomix + if (auto numberOpt = opcode.firstParameter()) { + unsigned number = *numberOpt; + if (number < 1 || number > config::maxEffectBuses) + break; + if (auto valueOpt = readOpcode(opcode.value, {0, 100})) + getOrCreateBus(number).setGainToMix(*valueOpt / 100); + } + break; + } + } + + unsigned busIndex; + if (busName.empty() || busName == "main") + busIndex = 0; + else if (busName.size() > 2 && busName.substr(0, 2) == "fx" && + absl::SimpleAtoi(busName.substr(2), &busIndex) && + busIndex >= 1 && busIndex <= config::maxEffectBuses) { + // an effect bus fxN, with N usually in [1,4] + } + else { + DBG("Unsupported effect bus: " << busName); + return; + } + + // create the effect and add it + EffectBus& bus = getOrCreateBus(busIndex); + Effect* fx = effectFactory.makeEffect(members); + bus.addEffect(std::unique_ptr(fx)); + + fx->init(sampleRate); +} + void addEndpointsToVelocityCurve(sfz::Region& region) { if (region.velocityPoints.size() > 0) { @@ -385,6 +462,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept this->samplesPerBlock = samplesPerBlock; this->tempBuffer.resize(samplesPerBlock); + this->tempMixNodeBuffer.resize(samplesPerBlock); for (auto& voice : voices) voice->setSamplesPerBlock(samplesPerBlock); } @@ -402,13 +480,17 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept resources.filterPool.setSampleRate(sampleRate); resources.eqPool.setSampleRate(sampleRate); + + for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { + if (EffectBus* bus = effectBuses[i].get()) + bus->init(sampleRate); + } } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { ScopedFTZ ftz; - if (freeWheeling) resources.filePool.waitForBackgroundLoading(); @@ -416,32 +498,71 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (!canEnterCallback) return; + size_t numFrames = buffer.getNumFrames(); + size_t numEffectBuses = effectBuses.size(); + auto temp = AudioSpan(tempBuffer).first(numFrames); + auto tempMixNode = AudioSpan(tempMixNodeBuffer).first(numFrames); + + // Prepare the effect inputs. They are mixes of per-region outputs. + for (size_t i = 0; i < numEffectBuses; ++i) { + if (EffectBus* bus = effectBuses[i].get()) + bus->clearInputs(numFrames); + } + + // CallbackBreakdown callbackBreakdown; int numActiveVoices { 0 }; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod }; buffer.fill(0.0f); + tempMixNode.fill(0.0f); resources.filePool.cleanupPromises(); - - auto tempSpan = AudioSpan(tempBuffer).first(buffer.getNumFrames()); for (auto& voice : voices) { - if (!voice->isFree()) { - numActiveVoices++; - voice->renderBlock(tempSpan); - buffer.add(tempSpan); - callbackBreakdown.data += voice->getLastDataDuration(); - callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); - callbackBreakdown.filters += voice->getLastFilterDuration(); - callbackBreakdown.panning += voice->getLastPanningDuration(); - } - } + if (voice->isFree()) + continue; - buffer.applyGain(db2mag(volume)); + const Region* region = voice->getRegion(); + + numActiveVoices++; + voice->renderBlock(temp); + + // Add the output into the effects linked to this region + for (size_t i = 0; i < numEffectBuses; ++i) { + if (EffectBus* bus = effectBuses[i].get()) { + float addGain = region->getGainToEffectBus(i); + bus->addToInputs(temp, addGain, numFrames); + } + } + + callbackBreakdown.data += voice->getLastDataDuration(); + callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); + callbackBreakdown.filters += voice->getLastFilterDuration(); + callbackBreakdown.panning += voice->getLastPanningDuration(); + } } + // Apply effect buses + // -- note(jpc) there is always a "main" bus which is initially empty. + // without any , the signal is just going to flow through it. + for (size_t i = 0; i < numEffectBuses; ++i) { + if (EffectBus* bus = effectBuses[i].get()) { + bus->process(numFrames); + bus->mixOutputsTo(buffer, tempMixNode, numFrames); + } + } + + // Add the Mix output (fxNtomix opcodes) + // -- note(jpc) the purpose of the Mix output is not known. + // perhaps it's designed as extension point for custom processing? + // as default behavior, it adds itself to the Main signal. + buffer.add(tempMixNode); + + // Apply the master volume + buffer.applyGain(db2mag(volume)); + callbackBreakdown.dispatch = dispatchDuration; - resources.logger.logCallbackTime(std::move(callbackBreakdown), numActiveVoices, buffer.getNumFrames()); + resources.logger.logCallbackTime(std::move(callbackBreakdown), numActiveVoices, numFrames); // Reset the dispatch counter dispatchDuration = Duration(0); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index f2c6fd11..3f94262d 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -9,6 +9,7 @@ #include "Parser.h" #include "Voice.h" #include "Region.h" +#include "Effects.h" #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" @@ -400,6 +401,12 @@ private: * @param members the opcodes of the block */ void handleControlOpcodes(const std::vector& members); + /** + * @brief Helper function to dispatch opcodes + * + * @param members the opcodes of the block + */ + void handleEffectOpcodes(const std::vector& members); /** * @brief Helper function to merge all the currently active opcodes * as set by the successive callbacks and create a new region to store @@ -441,8 +448,14 @@ private: std::array noteActivationLists; std::array ccActivationLists; - // Internal temporary buffer + // Effect factory and buses + EffectFactory effectFactory; + typedef std::unique_ptr EffectBusPtr; + std::vector effectBuses; // 0 is "main", 1-N are "fx1"-"fxN" + + // Intermediate buffers AudioBuffer tempBuffer { 2, config::defaultSamplesPerBlock }; + AudioBuffer tempMixNodeBuffer { 2, config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; diff --git a/src/sfizz/effects/Lofi.cpp b/src/sfizz/effects/Lofi.cpp new file mode 100644 index 00000000..9c4e1f7e --- /dev/null +++ b/src/sfizz/effects/Lofi.cpp @@ -0,0 +1,213 @@ +// 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 + +/** + Note(jpc): implementation status + +- [x] bitred +- [ ] bitred_oncc +- [ ] bitred_smoothcc +- [ ] bitred_stepcc +- [ ] bitred_curvecc + +- [x] decim +- [ ] decim_oncc +- [ ] decim_smoothcc +- [ ] decim_stepcc +- [ ] decim_curvecc + +- [ ] egN_bitred +- [ ] egN_bitred_oncc +- [ ] lfoN_bitred +- [ ] lfoN_bitred_oncc +- [ ] lfoN_bitred_smoothcc +- [ ] lfoN_bitred_stepcc + +- [ ] egN_decim +- [ ] egN_decim_oncc +- [ ] lfoN_decim +- [ ] lfoN_decim_oncc +- [ ] lfoN_decim_smoothcc +- [ ] lfoN_decim_stepcc + + */ + +#include "Lofi.h" +#include "Opcode.h" +#include +#include +#include +#include + +namespace sfz { +namespace fx { + + void Lofi::init(double sampleRate) + { + for (unsigned c = 0; c < EffectChannels; ++c) { + _bitred[c].init(sampleRate); + _decim[c].init(sampleRate); + } + } + + void Lofi::clear() + { + for (unsigned c = 0; c < EffectChannels; ++c) { + _bitred[c].clear(); + _decim[c].clear(); + } + } + + void Lofi::process(const float* const inputs[2], float* const outputs[2], unsigned nframes) + { + for (unsigned c = 0; c < EffectChannels; ++c) { + _bitred[c].setDepth(_bitred_depth); + _bitred[c].process(inputs[c], outputs[c], nframes); + + _decim[c].setDepth(_decim_depth); + _decim[c].process(outputs[c], outputs[c], nframes); + } + } + + Effect* Lofi::makeInstance(absl::Span members) + { + std::unique_ptr fx { new Lofi }; + + for (const Opcode& opcode : members) { + switch (opcode.lettersOnlyHash) { + case hash("bitred"): + setValueFromOpcode(opcode, fx->_bitred_depth, { 0.0, 100.0 }); + break; + case hash("decim"): + setValueFromOpcode(opcode, fx->_decim_depth, { 0.0, 100.0 }); + break; + } + } + + return fx.release(); + } + + /// + void Lofi::Bitred::init(double sampleRate) + { + (void)sampleRate; + + static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 }; + fDownsampler2x.set_coefs(coefs2x); + } + + void Lofi::Bitred::clear() + { + fLastValue = 0.0; + fDownsampler2x.clear_buffers(); + } + + void Lofi::Bitred::setDepth(float depth) + { + fDepth = std::max(0.0f, std::min(100.0f, depth)); + } + + void Lofi::Bitred::process(const float* in, float* out, uint32_t nframes) + { + float depth = fDepth; + + if (depth == 0) { + if (in != out) + std::memcpy(out, in, nframes * sizeof(float)); + clear(); + return; + } + + float lastValue = fLastValue; + hiir::Downsampler2xFpu<12>& downsampler2x = fDownsampler2x; + + float steps = (1.0f + (100.0f - depth)) * 0.75f; + + for (uint32_t i = 0; i < nframes; ++i) { + float x = in[i]; + + float y = std::copysign((int)(0.5f + std::fabs(x * steps)), x) * (1 / steps); + + float y2x[2]; + y2x[0] = (y != lastValue) ? (0.5f * (y + lastValue)) : y; + y2x[1] = y; + + lastValue = y; + + y = downsampler2x.process_sample(y2x); + out[i] = y; + } + + fLastValue = lastValue; + } + + /// + void Lofi::Decim::init(double sampleRate) + { + fSampleTime = 1.0 / sampleRate; + + static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 }; + fDownsampler2x.set_coefs(coefs2x); + } + + void Lofi::Decim::clear() + { + fPhase = 0.0; + fLastValue = 0.0; + fDownsampler2x.clear_buffers(); + } + + void Lofi::Decim::setDepth(float depth) + { + fDepth = std::max(0.0f, std::min(100.0f, depth)); + } + + void Lofi::Decim::process(const float* in, float* out, uint32_t nframes) + { + float depth = fDepth; + + if (depth == 0) { + if (in != out) + std::memcpy(out, in, nframes * sizeof(float)); + clear(); + return; + } + + float dt; + { + // exponential curve fit + float a = 1.289079e+00, b = 1.384141e-01, c = 1.313298e-04; + dt = std::pow(a, b * depth) * c - c; + dt = fSampleTime / dt; + } + + float phase = fPhase; + float lastValue = fLastValue; + hiir::Downsampler2xFpu<12>& downsampler2x = fDownsampler2x; + + for (uint32_t i = 0; i < nframes; ++i) { + float x = in[i]; + + phase += dt; + float y = (phase > 1.0f) ? x : lastValue; + phase -= (int)phase; + + float y2x[2]; + y2x[0] = (y != lastValue) ? (0.5f * (y + lastValue)) : y; + y2x[1] = y; + + lastValue = y; + + y = downsampler2x.process_sample(y2x); + out[i] = y; + } + + fPhase = phase; + fLastValue = lastValue; + } + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Lofi.h b/src/sfizz/effects/Lofi.h new file mode 100644 index 00000000..8bfda5d8 --- /dev/null +++ b/src/sfizz/effects/Lofi.h @@ -0,0 +1,79 @@ +// 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 "Effects.h" +#include "hiir/Downsampler2xFpu.h" + +namespace sfz { +namespace fx { + + /** + * @brief Bit crushing effect + */ + class Lofi : public Effect { + public: + /** + * @brief Initializes with the given sample rate. + */ + void init(double sampleRate) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Computes a cycle of the effect in stereo. + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + + /** + * @brief Instantiates given the contents of the block. + */ + static Effect* makeInstance(absl::Span members); + + private: + float _bitred_depth = 0; + float _decim_depth = 0; + + /// + class Bitred { + public: + void init(double sampleRate); + void clear(); + void setDepth(float depth); + void process(const float* in, float* out, uint32_t nframes); + + private: + float fDepth = 0.0; + float fLastValue = 0.0; + hiir::Downsampler2xFpu<12> fDownsampler2x; + }; + + /// + class Decim { + public: + void init(double sampleRate); + void clear(); + void setDepth(float depth); + void process(const float* in, float* out, uint32_t nframes); + + private: + float fSampleTime = 0.0; + float fDepth = 0.0; + float fPhase = 0.0; + float fLastValue = 0.0; + hiir::Downsampler2xFpu<12> fDownsampler2x; + }; + + /// + Bitred _bitred[EffectChannels]; + Decim _decim[EffectChannels]; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Nothing.cpp b/src/sfizz/effects/Nothing.cpp new file mode 100644 index 00000000..527ce5ae --- /dev/null +++ b/src/sfizz/effects/Nothing.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 "Nothing.h" +#include + +namespace sfz { +namespace fx { + + void Nothing::init(double sampleRate) + { + (void)sampleRate; + } + + void Nothing::clear() + { + } + + void Nothing::process(const float* const inputs[], float* const outputs[], unsigned nframes) + { + for (unsigned c = 0; c < EffectChannels; ++c) { + if (inputs[c] != outputs[c]) + std::memcpy(outputs[c], inputs[c], nframes * sizeof(float)); + } + } + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Nothing.h b/src/sfizz/effects/Nothing.h new file mode 100644 index 00000000..239697d8 --- /dev/null +++ b/src/sfizz/effects/Nothing.h @@ -0,0 +1,35 @@ +// 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 "Effects.h" + +namespace sfz { +namespace fx { + + /** + * @brief Effect which does nothing + */ + class Nothing : public Effect { + public: + /** + * @brief Initializes with the given sample rate. + */ + void init(double sampleRate) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Copy the input signal to the output + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + }; + +} // namespace fx +} // namespace sfz