Initial effects

This commit is contained in:
Jean Pierre Cimalando 2020-02-26 21:09:34 +01:00 committed by Paul Fd
parent 291920f263
commit 28c12f32f3
13 changed files with 852 additions and 18 deletions

View file

@ -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)

View file

@ -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<Type> corresponding to a specific channel
*

View file

@ -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

137
src/sfizz/Effects.cpp Normal file
View file

@ -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 <algorithm>
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<const Opcode> 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<Effect> fx)
{
_effects.emplace_back(std::move(fx));
}
void EffectBus::clearInputs(unsigned nframes)
{
AudioSpan<float>(_inputs).first(nframes).fill(0.0f);
AudioSpan<float>(_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<const float> 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<float>(_inputs), AudioSpan<float>(_outputs), nframes);
for (size_t i = 1; i < numEffects; ++i)
_effects[i]->process(
AudioSpan<float>(_outputs), AudioSpan<float>(_outputs), nframes);
} else
fx::Nothing().process(
AudioSpan<float>(_inputs), AudioSpan<float>(_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<const float> fxOut = _outputs.getConstSpan(c);
sfz::multiplyAdd(gainToMain, fxOut, absl::Span<float>(mainOutput[c], nframes));
sfz::multiplyAdd(gainToMix, fxOut, absl::Span<float>(mixOutput[c], nframes));
}
}
} // namespace sfz

147
src/sfizz/Effects.h Normal file
View file

@ -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 <array>
#include <vector>
#include <memory>
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 <effect> block
*/
typedef Effect* (MakeInstance)(absl::Span<const Opcode> 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 <effect> block.
*/
Effect* makeEffect(absl::Span<const Opcode> members);
private:
struct FactoryEntry {
std::string name;
Effect::MakeInstance* make;
};
std::vector<FactoryEntry> _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<Effect> 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<std::unique_ptr<Effect>> _effects;
AudioBuffer<float> _inputs { EffectChannels, config::defaultSamplesPerBlock };
AudioBuffer<float> _outputs { EffectChannels, config::defaultSamplesPerBlock };
float _gainToMain = 0.0;
float _gainToMix = 0.0;
};
} // namespace sfz

View file

@ -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<float>(opcode.value, {0, 100});
if (!value)
break;
if (static_cast<size_t>(*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];
}

View file

@ -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<float> gainToEffect;
private:
const MidiState& midiState;
bool keySwitched { true };

View file

@ -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<Opcode>& 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<Opcode>& members)
}
}
void sfz::Synth::handleEffectOpcodes(const std::vector<Opcode>& 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<float>(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<float>(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<float>(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<Effect>(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<float> buffer) noexcept
{
ScopedFTZ ftz;
if (freeWheeling)
resources.filePool.waitForBackgroundLoading();
@ -416,32 +498,71 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
if (!canEnterCallback)
return;
size_t numFrames = buffer.getNumFrames();
size_t numEffectBuses = effectBuses.size();
auto temp = AudioSpan<float>(tempBuffer).first(numFrames);
auto tempMixNode = AudioSpan<float>(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<float>(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 <effect>, 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);

View file

@ -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 <control> block
*/
void handleControlOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <effect> opcodes
*
* @param members the opcodes of the <effect> block
*/
void handleEffectOpcodes(const std::vector<Opcode>& 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<RegionPtrVector, 128> noteActivationLists;
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
// Internal temporary buffer
// Effect factory and buses
EffectFactory effectFactory;
typedef std::unique_ptr<EffectBus> EffectBusPtr;
std::vector<EffectBusPtr> effectBuses; // 0 is "main", 1-N are "fx1"-"fxN"
// Intermediate buffers
AudioBuffer<float> tempBuffer { 2, config::defaultSamplesPerBlock };
AudioBuffer<float> tempMixNodeBuffer { 2, config::defaultSamplesPerBlock };
int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate };

213
src/sfizz/effects/Lofi.cpp Normal file
View file

@ -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 <memory>
#include <algorithm>
#include <cstring>
#include <cmath>
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<const Opcode> members)
{
std::unique_ptr<Lofi> 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

79
src/sfizz/effects/Lofi.h Normal file
View file

@ -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 <effect> block.
*/
static Effect* makeInstance(absl::Span<const Opcode> 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

View file

@ -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 <cstring>
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

View file

@ -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