Strings resonator effect
This commit is contained in:
parent
f489b88df0
commit
c0127fe02f
6 changed files with 399 additions and 0 deletions
|
|
@ -25,6 +25,7 @@ set (SFIZZ_SOURCES
|
|||
sfizz/effects/Apan.cpp
|
||||
sfizz/effects/Lofi.cpp
|
||||
sfizz/effects/Limiter.cpp
|
||||
sfizz/effects/Strings.cpp
|
||||
sfizz/effects/Rectify.cpp
|
||||
sfizz/effects/Gain.cpp
|
||||
sfizz/effects/Width.cpp
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include "effects/Apan.h"
|
||||
#include "effects/Lofi.h"
|
||||
#include "effects/Limiter.h"
|
||||
#include "effects/Strings.h"
|
||||
#include "effects/Rectify.h"
|
||||
#include "effects/Gain.h"
|
||||
#include "effects/Width.h"
|
||||
|
|
@ -30,6 +31,7 @@ void EffectFactory::registerStandardEffectTypes()
|
|||
registerEffectType("apan", fx::Apan::makeInstance);
|
||||
registerEffectType("lofi", fx::Lofi::makeInstance);
|
||||
registerEffectType("limiter", fx::Limiter::makeInstance);
|
||||
registerEffectType("strings", fx::Strings::makeInstance);
|
||||
|
||||
// extensions (book)
|
||||
registerEffectType("rectify", fx::Rectify::makeInstance);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ constexpr T min(T op1, Args... rest)
|
|||
return min(op1, min(rest...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compute the square of the value
|
||||
*
|
||||
* @param op
|
||||
* @return T
|
||||
*/
|
||||
template<class T>
|
||||
constexpr T power2(T in)
|
||||
{
|
||||
return in * in;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts db values into power (applies 10**(in/10))
|
||||
*
|
||||
|
|
|
|||
145
src/sfizz/effects/Strings.cpp
Normal file
145
src/sfizz/effects/Strings.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// 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] strings_number
|
||||
- [ ] strings_wet_oncc
|
||||
|
||||
Extensions
|
||||
- [x] strings_wet
|
||||
*/
|
||||
|
||||
#include "Strings.h"
|
||||
#include "StringsPrivate.h"
|
||||
#include "Opcode.h"
|
||||
#include "MathHelpers.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace sfz {
|
||||
namespace fx {
|
||||
|
||||
struct Strings::ResonantString {
|
||||
Bw2BPF bpf;
|
||||
WgResonator res;
|
||||
};
|
||||
|
||||
Strings::Strings()
|
||||
: _strings(new ResonantString[MaximumNumStrings])
|
||||
{
|
||||
}
|
||||
|
||||
Strings::~Strings()
|
||||
{
|
||||
}
|
||||
|
||||
void Strings::setSampleRate(double sampleRate)
|
||||
{
|
||||
for (unsigned i = 0, n = _numStrings; i < n; ++i) {
|
||||
ResonantString& rs = _strings[i];
|
||||
rs.bpf.init(sampleRate);
|
||||
rs.res.init(sampleRate);
|
||||
|
||||
int midiNote = i + 24;
|
||||
double midiFrequency = 440.0 * std::exp2((midiNote - 69) * (1.0 / 12.0));
|
||||
|
||||
// 1 Hz works decently as compromise of selectivity/speed
|
||||
double bpfBandwidth = 1.0;
|
||||
rs.bpf.setCutoff(
|
||||
midiFrequency - 0.5 * bpfBandwidth,
|
||||
midiFrequency + 0.5 * bpfBandwidth);
|
||||
|
||||
rs.res.setFrequency(midiFrequency);
|
||||
|
||||
// TODO(jpc) find how to adjust the string feedbacks
|
||||
// for now set a fixed release time for all strings
|
||||
double releaseTime = 50e-3;
|
||||
double releaseFeedback = std::exp(-6.91 / (releaseTime * sampleRate));
|
||||
rs.res.setFeedback(releaseFeedback);
|
||||
}
|
||||
}
|
||||
|
||||
void Strings::setSamplesPerBlock(int samplesPerBlock)
|
||||
{
|
||||
_tempBuffer.resize(samplesPerBlock);
|
||||
}
|
||||
|
||||
void Strings::clear()
|
||||
{
|
||||
for (unsigned i = 0, n = _numStrings; i < n; ++i) {
|
||||
ResonantString& rs = _strings[i];
|
||||
rs.bpf.clear();
|
||||
rs.res.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Strings::process(const float* const inputs[], float* const outputs[], unsigned nframes)
|
||||
{
|
||||
auto inputL = absl::MakeConstSpan(inputs[0], nframes);
|
||||
auto inputR = absl::MakeConstSpan(inputs[1], nframes);
|
||||
|
||||
// mix down the stereo signal to create the resonator excitation source
|
||||
absl::Span<float> resInput = _tempBuffer.getSpan(0).first(nframes);
|
||||
sfz::applyGain<float>(M_SQRT1_2, inputL, resInput);
|
||||
sfz::multiplyAdd<float>(M_SQRT1_2, inputR, resInput);
|
||||
|
||||
// generate the strings summed into a common buffer
|
||||
absl::Span<float> resOutput = _tempBuffer.getSpan(1).first(nframes);
|
||||
sfz::fill(resOutput, 0.0f);
|
||||
|
||||
for (unsigned is = 0, ns = _numStrings; is < ns; ++is) {
|
||||
ResonantString& rs = _strings[is];
|
||||
for (unsigned i = 0; i < nframes; ++i) {
|
||||
float sample = resInput[i];
|
||||
sample = rs.bpf.process(sample);
|
||||
sample = rs.res.process(sample);
|
||||
resOutput[i] += sample;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jpc) damping of the high frequencies
|
||||
// it's easiest apply individual gains to resonating strings
|
||||
// or pass resonator output through LPF
|
||||
|
||||
// mix the resonator into the output
|
||||
auto outputL = absl::MakeSpan(outputs[0], nframes);
|
||||
auto outputR = absl::MakeSpan(outputs[1], nframes);
|
||||
|
||||
constexpr float resAttenuate = 1e-3; // need significant attenuation, here -60dB
|
||||
|
||||
absl::Span<float> wet = _tempBuffer.getSpan(2).first(nframes);
|
||||
sfz::fill(wet, 0.01f * resAttenuate *_wet); // TOD strings_wet_oncc modulation...
|
||||
|
||||
sfz::copy(inputL, outputL);
|
||||
sfz::copy(inputR, outputR);
|
||||
sfz::multiplyAdd<float>(wet, resOutput, outputL);
|
||||
sfz::multiplyAdd<float>(wet, resOutput, outputR);
|
||||
}
|
||||
|
||||
std::unique_ptr<Effect> Strings::makeInstance(absl::Span<const Opcode> members)
|
||||
{
|
||||
auto fx = absl::make_unique<Strings>();
|
||||
|
||||
for (const Opcode& opc : members) {
|
||||
switch (opc.lettersOnlyHash) {
|
||||
case hash("strings_number"):
|
||||
setValueFromOpcode(opc, fx->_numStrings, {0, MaximumNumStrings});
|
||||
break;
|
||||
case hash("strings_wet"):
|
||||
setValueFromOpcode(opc, fx->_wet, {0.0f, 100.0f});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return CXX11_MOVE(fx);
|
||||
}
|
||||
|
||||
} // namespace fx
|
||||
} // namespace sfz
|
||||
65
src/sfizz/effects/Strings.h
Normal file
65
src/sfizz/effects/Strings.h
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// 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 "AudioBuffer.h"
|
||||
#include <memory>
|
||||
|
||||
namespace sfz {
|
||||
namespace fx {
|
||||
|
||||
class Bw2BPF;
|
||||
class WgResonator;
|
||||
|
||||
/**
|
||||
* @brief String resonance effect
|
||||
*/
|
||||
class Strings : public Effect {
|
||||
public:
|
||||
Strings();
|
||||
~Strings();
|
||||
|
||||
/**
|
||||
* @brief Initializes with the given sample rate.
|
||||
*/
|
||||
void setSampleRate(double sampleRate) override;
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum number of frames to render at a time. The actual
|
||||
* value can be lower but should never be higher.
|
||||
*/
|
||||
void setSamplesPerBlock(int samplesPerBlock) 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;
|
||||
|
||||
/**
|
||||
* @brief Instantiates given the contents of the <effect> block.
|
||||
*/
|
||||
static std::unique_ptr<Effect> makeInstance(absl::Span<const Opcode> members);
|
||||
|
||||
private:
|
||||
enum { MaximumNumStrings = 88 };
|
||||
|
||||
unsigned _numStrings = MaximumNumStrings;
|
||||
float _wet = 0;
|
||||
|
||||
struct ResonantString;
|
||||
std::unique_ptr<ResonantString[]> _strings;
|
||||
|
||||
AudioBuffer<float, 3> _tempBuffer { 3, config::defaultSamplesPerBlock };
|
||||
};
|
||||
|
||||
} // namespace fx
|
||||
} // namespace sfz
|
||||
174
src/sfizz/effects/StringsPrivate.h
Normal file
174
src/sfizz/effects/StringsPrivate.h
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// 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 "MathHelpers.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace sfz {
|
||||
namespace fx {
|
||||
|
||||
// Butterworth 2nd order bandpass (faust -double -os)
|
||||
/*
|
||||
import("stdfaust.lib");
|
||||
process = fi.bandpass(1, loF, hiF) with {
|
||||
loF = hslider("[1] Lo frequency [unit:Hz]", 1, 0, 1000, 1);
|
||||
hiF = hslider("[2] Hi frequency [unit:Hz]", 1, 0, 1000, 1);
|
||||
};
|
||||
*/
|
||||
class Bw2BPF {
|
||||
private:
|
||||
typedef float FAUSTFLOAT;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize.
|
||||
*/
|
||||
void init(double sampleRate)
|
||||
{
|
||||
fConst0 = sampleRate;
|
||||
fConst1 = (2.0 / fConst0);
|
||||
fConst2 = (2.0 * fConst0);
|
||||
fConst3 = (3.1415926535897931 / fConst0);
|
||||
fConst4 = (0.5 / fConst0);
|
||||
fConst5 = (4.0 * power2(fConst0));
|
||||
fConst6 = power2((1.0 / fConst0));
|
||||
fConst7 = (2.0 * fConst6);
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the memory of the filter.
|
||||
*/
|
||||
void clear()
|
||||
{
|
||||
for (int l0 = 0; (l0 < 3); l0 = (l0 + 1)) {
|
||||
fRec0[l0] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the BPF low and high frequencies for -3dB response.
|
||||
*
|
||||
* The center frequency is (loF+hiF)/2.
|
||||
*/
|
||||
void setCutoff(double loF, double hiF)
|
||||
{
|
||||
fControl[0] = std::tan((fConst3 * double(hiF)));
|
||||
fControl[1] = power2(std::sqrt((fConst5 * (fControl[0] * std::tan((fConst3 * double(loF)))))));
|
||||
fControl[2] = ((fConst2 * fControl[0]) - (fConst4 * (fControl[1] / fControl[0])));
|
||||
fControl[3] = (fConst6 * fControl[1]);
|
||||
fControl[4] = (fConst1 * fControl[2]);
|
||||
fControl[5] = ((fControl[3] + fControl[4]) + 4.0);
|
||||
fControl[6] = (fConst1 * (fControl[2] / fControl[5]));
|
||||
fControl[7] = (1.0 / fControl[5]);
|
||||
fControl[8] = ((fConst7 * fControl[1]) + -8.0);
|
||||
fControl[9] = (fControl[3] + (4.0 - fControl[4]));
|
||||
fControl[10] = (0.0 - fControl[6]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the next filtered sample.
|
||||
*/
|
||||
FAUSTFLOAT process(FAUSTFLOAT input)
|
||||
{
|
||||
fRec0[0] = (double(input) - (fControl[7] * ((fControl[8] * fRec0[1]) + (fControl[9] * fRec0[2]))));
|
||||
FAUSTFLOAT output = FAUSTFLOAT(((fControl[6] * fRec0[0]) + (fControl[10] * fRec0[2])));
|
||||
fRec0[2] = fRec0[1];
|
||||
fRec0[1] = fRec0[0];
|
||||
return output;
|
||||
}
|
||||
|
||||
private:
|
||||
double fRec0[3] {};
|
||||
double fControl[11] {};
|
||||
double fConst0 {};
|
||||
double fConst1 {};
|
||||
double fConst2 {};
|
||||
double fConst3 {};
|
||||
double fConst4 {};
|
||||
double fConst5 {};
|
||||
double fConst6 {};
|
||||
double fConst7 {};
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Waveguide resonator (faust -os)
|
||||
/*
|
||||
import("stdfaust.lib");
|
||||
process = fi.nlf2(f, r) : (_,!) with {
|
||||
f = hslider("[1] Resonance frequency [unit:Hz]", 1, 0, 1000, 1);
|
||||
r = hslider("[2] Resonance feedback", 0, 0, 1, 1e-3);
|
||||
};
|
||||
*/
|
||||
class WgResonator {
|
||||
private:
|
||||
typedef float FAUSTFLOAT;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize.
|
||||
*/
|
||||
void init(float sampleRate)
|
||||
{
|
||||
fConst0 = (6.28318548f / sampleRate);
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the memory of the resonator.
|
||||
*/
|
||||
void clear()
|
||||
{
|
||||
for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) {
|
||||
fRec0[l0] = 0.0f;
|
||||
}
|
||||
for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) {
|
||||
fRec1[l1] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the resonance frequency.
|
||||
*/
|
||||
void setFrequency(float frequency)
|
||||
{
|
||||
fControl[1] = (fConst0 * float(frequency));
|
||||
fControl[2] = std::sin(fControl[1]);
|
||||
fControl[3] = std::cos(fControl[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the resonance feedback.
|
||||
*/
|
||||
void setFeedback(float feedback)
|
||||
{
|
||||
fControl[0] = float(feedback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the next resonance sample.
|
||||
*/
|
||||
FAUSTFLOAT process(FAUSTFLOAT input)
|
||||
{
|
||||
fRec0[0] = (fControl[0] * ((fControl[2] * fRec1[1]) + (fControl[3] * fRec0[1])));
|
||||
fRec1[0] = ((float(input) + (fControl[3] * fRec1[1])) - (fControl[2] * fRec0[1]));
|
||||
FAUSTFLOAT output = FAUSTFLOAT(fRec0[0]);
|
||||
fRec0[1] = fRec0[0];
|
||||
fRec1[1] = fRec1[0];
|
||||
return output;
|
||||
}
|
||||
|
||||
private:
|
||||
float fRec0[2] {};
|
||||
float fRec1[2] {};
|
||||
float fControl[4];
|
||||
float fConst0 {};
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
} // namespace fx
|
||||
Loading…
Add table
Reference in a new issue