diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 76d7d7ab..d24083a9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index 9facbfcb..e6dd469d 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.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); diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index b43428f8..960ccb73 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -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 +constexpr T power2(T in) +{ + return in * in; +} + /** * @brief Converts db values into power (applies 10**(in/10)) * diff --git a/src/sfizz/effects/Strings.cpp b/src/sfizz/effects/Strings.cpp new file mode 100644 index 00000000..6073e753 --- /dev/null +++ b/src/sfizz/effects/Strings.cpp @@ -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 + +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 resInput = _tempBuffer.getSpan(0).first(nframes); + sfz::applyGain(M_SQRT1_2, inputL, resInput); + sfz::multiplyAdd(M_SQRT1_2, inputR, resInput); + + // generate the strings summed into a common buffer + absl::Span 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 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(wet, resOutput, outputL); + sfz::multiplyAdd(wet, resOutput, outputR); + } + + std::unique_ptr Strings::makeInstance(absl::Span members) + { + auto fx = absl::make_unique(); + + 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 diff --git a/src/sfizz/effects/Strings.h b/src/sfizz/effects/Strings.h new file mode 100644 index 00000000..a628960a --- /dev/null +++ b/src/sfizz/effects/Strings.h @@ -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 + +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 block. + */ + static std::unique_ptr makeInstance(absl::Span members); + + private: + enum { MaximumNumStrings = 88 }; + + unsigned _numStrings = MaximumNumStrings; + float _wet = 0; + + struct ResonantString; + std::unique_ptr _strings; + + AudioBuffer _tempBuffer { 3, config::defaultSamplesPerBlock }; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/StringsPrivate.h b/src/sfizz/effects/StringsPrivate.h new file mode 100644 index 00000000..e17a8b59 --- /dev/null +++ b/src/sfizz/effects/StringsPrivate.h @@ -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 + +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