From d973a85d274c61f75e9886efb9c72cf36dc121b0 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Mon, 9 Sep 2024 18:20:39 +0900 Subject: [PATCH] Update DoubleLoopCymbal (WIP) --- DoubleLoopCymbal/source/dsp/delay.hpp | 96 ++++++++++--------------- DoubleLoopCymbal/source/dsp/dspcore.cpp | 46 ++++++------ DoubleLoopCymbal/source/dsp/dspcore.hpp | 8 +-- DoubleLoopCymbal/source/editor.cpp | 27 ++++--- DoubleLoopCymbal/source/parameter.cpp | 5 +- DoubleLoopCymbal/source/parameter.hpp | 35 +++++---- 6 files changed, 95 insertions(+), 122 deletions(-) diff --git a/DoubleLoopCymbal/source/dsp/delay.hpp b/DoubleLoopCymbal/source/dsp/delay.hpp index db0ea4da..43f74272 100644 --- a/DoubleLoopCymbal/source/dsp/delay.hpp +++ b/DoubleLoopCymbal/source/dsp/delay.hpp @@ -30,10 +30,29 @@ namespace SomeDSP { template class ExpDecay { public: + // static constexpr Sample alpha = Sample(1.1920928955078125e-7); // 2^-23. + // static constexpr Sample normalizeGain = Sample(15.942385152878742); // -log(2^-23). + Sample value = 0; Sample alpha = 0; - void setTime(Sample decayTimeInSamples, bool sustain = false) + void setTime(Sample decayTimeInSamples) + { + constexpr auto eps = Sample(std::numeric_limits::epsilon()); + alpha = std::pow(eps, Sample(1) / decayTimeInSamples); + } + + void reset() { value = 0; } + void trigger(Sample gain = Sample(1)) { value = gain; } + Sample process() { return value *= alpha; } +}; + +template class ExpSREnvelope { +public: + Sample value = 0; + Sample alpha = 0; + + void setTime(Sample decayTimeInSamples, bool sustain) { constexpr auto eps = Sample(std::numeric_limits::epsilon()); alpha = sustain ? Sample(1) : std::pow(eps, Sample(1) / decayTimeInSamples); @@ -304,46 +323,7 @@ template class EmaLowShelf { } }; -template class AdaptiveNotchCPZ { -public: - static constexpr Sample mu = Sample(2) / Sample(1024); - Sample alpha = Sample(-2); - - Sample v1 = 0; - Sample v2 = 0; - - void reset() - { - alpha = Sample(-2); // 0 Hz as initial guess. - - v1 = 0; - v2 = 0; - } - - Sample process(Sample input, Sample narrowness) - { - const auto a1 = narrowness * alpha; - const auto a2 = narrowness * narrowness; - auto gain = alpha >= 0 ? (Sample(1) + a1 + a2) / (Sample(2) + alpha) - : (Sample(1) - a1 + a2) / (Sample(2) - alpha); - - constexpr auto clip = Sample(1) / std::numeric_limits::epsilon(); - const auto x0 = std::clamp(input, -clip, clip); - auto v0 = x0 - a1 * v1 - a2 * v2; - const auto y0 = v0 + alpha * v1 + v2; - const auto s0 - = (Sample(1) - narrowness) * v0 - narrowness * (Sample(1) - narrowness) * v2; - constexpr auto bound = Sample(2); - alpha = std::clamp(alpha - y0 * s0 * mu, -bound, bound); - - v2 = v1; - v1 = v0; - - return y0 * gain; - } -}; - -template class SerialAllpass { +template class SerialAllpass { private: std::array buffer{}; std::array, nAllpass> delay; @@ -351,8 +331,8 @@ template class SerialAl std::array, nAllpass> highpass; public: - std::array, nAdaptiveNotch> notch; static constexpr size_t size = nAllpass; + size_t nDelay = nAllpass; std::array timeInSamples{}; void setup(Sample maxTimeSamples) @@ -366,7 +346,6 @@ template class SerialAl for (auto &x : delay) x.reset(); for (auto &x : lowpass) x.reset(); for (auto &x : highpass) x.reset(); - for (auto &x : notch) x.reset(); } void applyGain(Sample gain) @@ -376,14 +355,25 @@ template class SerialAl Sample sum(Sample altSignMix) { + // // TODO + // Sample sumAlt = Sample(0); + // Sample sign = Sample(1); + // for (const auto &x : buffer) { + // sumAlt += x * sign; + // sign = -sign; + // } + // Sample sumDirect = std::accumulate(buffer.begin(), buffer.end(), Sample(0)); + // return std::lerp(sumDirect, sumAlt, altSignMix) / (Sample(2) * nAllpass); + Sample sumAlt = Sample(0); Sample sign = Sample(1); - for (const auto &x : buffer) { - sumAlt += x * sign; + for (size_t i = 0; i < nDelay; ++i) { + sumAlt += buffer[i] * sign; sign = -sign; } - Sample sumDirect = std::accumulate(buffer.begin(), buffer.end(), Sample(0)); - return std::lerp(sumDirect, sumAlt, altSignMix) / (Sample(2) * nAllpass); + Sample sumDirect + = std::accumulate(buffer.begin(), buffer.begin() + nDelay, Sample(0)); + return std::lerp(sumDirect, sumAlt, altSignMix) / (Sample(2) * nDelay); } Sample process( @@ -394,12 +384,9 @@ template class SerialAl Sample lowShelfGain, Sample gain, Sample pitchRatio, - Sample timeModAmount, - size_t nNotch, - Sample notchMix, - Sample notchNarrowness) + Sample timeModAmount) { - for (size_t idx = 0; idx < nAllpass; ++idx) { + for (size_t idx = 0; idx < nDelay; ++idx) { constexpr auto sign = 1; auto x0 = lowpass[idx].process(sign * input, highShelfCut, highShelfGain); // auto x0 = sign * input; @@ -411,11 +398,6 @@ template class SerialAl } // input = lowpass[0].process(input, highShelfCut, highShelfGain); - - for (size_t idx = 0; idx < nNotch; ++idx) { - input += notchMix * (notch[idx].process(input, notchNarrowness) - input); - } - return input; } }; diff --git a/DoubleLoopCymbal/source/dsp/dspcore.cpp b/DoubleLoopCymbal/source/dsp/dspcore.cpp index 31e9a811..8c877cd5 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.cpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.cpp @@ -191,14 +191,12 @@ void DSPCore::setup(double sampleRate) std::clamp(pv[ID::allpassFeed2]->getDouble(), double(-0.99999), double(0.99999))); \ allpassMixSpike.METHOD(pv[ID::allpassMixSpike]->getDouble()); \ allpassMixAltSign.METHOD(pv[ID::allpassMixAltSign]->getDouble()); \ - highShelfCutoff.METHOD(EMAFilter::cutoffToP(std::clamp( \ - pv[ID::highShelfFrequencyHz]->getDouble() / upRate, double(0), double(0.5)))); \ + highShelfCutoff.METHOD(EMAFilter::cutoffToP( \ + std::min(pv[ID::highShelfFrequencyHz]->getDouble() / upRate, double(0.5)))); \ highShelfGain.METHOD(pv[ID::highShelfGain]->getDouble()); \ - lowShelfCutoff.METHOD(EMAFilter::cutoffToP(std::clamp( \ - pv[ID::lowShelfFrequencyHz]->getDouble() / upRate, double(0), double(0.5)))); \ + lowShelfCutoff.METHOD(EMAFilter::cutoffToP( \ + std::min(pv[ID::lowShelfFrequencyHz]->getDouble() / upRate, double(0.5)))); \ lowShelfGain.METHOD(pv[ID::lowShelfGain]->getDouble()); \ - notchMix.METHOD(pv[ID::adaptiveNotchMix]->getDouble()); \ - notchNarrowness.METHOD(pv[ID::adaptiveNotchNarrowness]->getDouble()); \ stereoBalance.METHOD(pv[ID::stereoBalance]->getDouble()); \ stereoMerge.METHOD(pv[ID::stereoMerge]->getDouble() / double(2)); \ \ @@ -212,7 +210,7 @@ void DSPCore::setup(double sampleRate) \ if (!pv[ID::release]->getInt() && noteStack.empty()) { \ envelopeRelease.setTime( \ - double(8) * pv[ID::closeAttackSeconds]->getDouble() * upRate); \ + double(8) * pv[ID::closeAttackSeconds]->getDouble() * upRate, false); \ } \ \ envelopeClose.update( \ @@ -240,18 +238,23 @@ void DSPCore::updateDelayTime() const auto delayTimeBase = pv[ID::delayTimeBaseSecond]->getDouble() * upRate; const auto delayTimeRandom = pv[ID::delayTimeRandomSecond]->getDouble() * upRate; const auto shape = pv[ID::delayTimeShape]->getDouble(); - std::uniform_real_distribution delayTimeDist{ - double(0), double(delayTimeRandom)}; + const auto pitchRatio = std::exp2(pv[ID::delayTimeRatio]->getDouble() / double(-12)); + std::uniform_real_distribution timeDist{double(0), double(delayTimeRandom)}; for (size_t idx = 0; idx < nAllpass; ++idx) { static_assert(nAllpass >= 1); const auto t1 = delayTimeBase / double(idx + 1); const auto t2 = delayTimeBase * (circularModes[idx] / circularModes[nAllpass - 1]); - const auto timeHarmonics = std::lerp(t1, t2, shape); - serialAllpass1[0].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); - serialAllpass1[1].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); - serialAllpass2[0].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); - serialAllpass2[1].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); + const auto harmonics = std::lerp(t1, t2, shape); + serialAllpass1[0].timeInSamples[idx] = harmonics + timeDist(paramRng); + serialAllpass1[1].timeInSamples[idx] = harmonics + timeDist(paramRng); + serialAllpass2[0].timeInSamples[idx] = pitchRatio * (harmonics + timeDist(paramRng)); + serialAllpass2[1].timeInSamples[idx] = pitchRatio * (harmonics + timeDist(paramRng)); } + + const size_t nDelay1 = 1 + pv[ID::allpassDelayCount1]->getInt(); + const size_t nDelay2 = 1 + pv[ID::allpassDelayCount2]->getInt(); + for (auto &x : serialAllpass1) x.nDelay = nDelay1; + for (auto &x : serialAllpass2) x.nDelay = nDelay2; } void DSPCore::reset() @@ -325,9 +328,6 @@ std::array DSPCore::processFrame(const std::array &externa const auto hsGain = highShelfGain.process(); //* envRelease; const auto lsCut = lowShelfCutoff.process(); const auto lsGain = lowShelfGain.process(); - const size_t nNotch = param.value[ParameterID::nAdaptiveNotch]->getInt(); - const auto ntMix = notchMix.process(); - const auto ntNarrowness = notchNarrowness.process(); const auto balance = stereoBalance.process(); const auto merge = stereoMerge.process(); const auto outGain = outputGain.process() * envRelease; @@ -373,11 +373,9 @@ std::array DSPCore::processFrame(const std::array &externa * normalizeGain; feedbackBuffer1[0] = serialAllpass1[0].process( - excitation[0], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt, nNotch, - ntMix, ntNarrowness); + excitation[0], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt); feedbackBuffer1[1] = serialAllpass1[1].process( - excitation[1], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt, nNotch, - ntMix, ntNarrowness); + excitation[1], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt); auto cymbal0 = std::lerp(serialAllpass2[0].sum(apMixSign), feedbackBuffer2[0], apMixSpike) @@ -387,10 +385,10 @@ std::array DSPCore::processFrame(const std::array &externa * normalizeGain; feedbackBuffer2[0] = serialAllpass2[0].process( ap1Out0 - apGain2 * feedbackBuffer2[0], hsCut, hsGain, lsCut, lsGain, apGain2, - pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); + pitchRatio, timeModAmt); feedbackBuffer2[1] = serialAllpass2[1].process( ap1Out1 - apGain2 * feedbackBuffer2[1], hsCut, hsGain, lsCut, lsGain, apGain2, - pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); + pitchRatio, timeModAmt); constexpr auto eps = std::numeric_limits::epsilon(); if (balance < -eps) { @@ -508,7 +506,7 @@ void DSPCore::noteOff(int_fast32_t noteId) velocity = 0; envelopeHalfClosed.release(); if (!pv[ID::release]->getInt()) { - envelopeRelease.setTime(releaseTimeSecond * upRate); + envelopeRelease.setTime(releaseTimeSecond * upRate, false); } } } diff --git a/DoubleLoopCymbal/source/dsp/dspcore.hpp b/DoubleLoopCymbal/source/dsp/dspcore.hpp index 8e3254ed..219916f7 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.hpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.hpp @@ -129,8 +129,6 @@ class DSPCore { ExpSmoother highShelfGain; ExpSmoother lowShelfCutoff; ExpSmoother lowShelfGain; - ExpSmoother notchMix; - ExpSmoother notchNarrowness; ExpSmoother stereoBalance; ExpSmoother stereoMerge; ExpSmoother outputGain; @@ -143,13 +141,13 @@ class DSPCore { TransitionReleaseSmoother releaseSmoother; ExpDecay envelopeNoise; ExpDSREnvelope envelopeHalfClosed; - ExpDecay envelopeRelease; + ExpSREnvelope envelopeRelease; ExpADEnvelope envelopeClose; std::array, 2> halfClosedNoise; std::array feedbackBuffer1{}; std::array feedbackBuffer2{}; - std::array, 2> serialAllpass1; - std::array, 2> serialAllpass2; + std::array, 2> serialAllpass1; + std::array, 2> serialAllpass2; std::array, 2> halfbandInput{}; std::array>, 2> halfbandIir; diff --git a/DoubleLoopCymbal/source/editor.cpp b/DoubleLoopCymbal/source/editor.cpp index f43bad5e..3bc82487 100644 --- a/DoubleLoopCymbal/source/editor.cpp +++ b/DoubleLoopCymbal/source/editor.cpp @@ -353,25 +353,24 @@ bool Editor::prepareUI() thirdLeft1, thirdTop3, labelWidth, labelHeight, uiTextSize, ID::delayTimeRandomSecond, Scales::delayTimeSecond, false, 5); addLabel( - thirdLeft0, thirdTop4, labelWidth, labelHeight, uiTextSize, "Modulation [sample]"); + thirdLeft0, thirdTop4, labelWidth, labelHeight, uiTextSize, "Pitch Ratio [st.]"); addTextKnob( - thirdLeft1, thirdTop4, labelWidth, labelHeight, uiTextSize, ID::delayTimeModAmount, + thirdLeft1, thirdTop4, labelWidth, labelHeight, uiTextSize, ID::delayTimeRatio, + Scales::semitone, false, 5); + addLabel( + thirdLeft0, thirdTop5, labelWidth, labelHeight, uiTextSize, "Modulation [sample]"); + addTextKnob( + thirdLeft1, thirdTop5, labelWidth, labelHeight, uiTextSize, ID::delayTimeModAmount, Scales::delayTimeModAmount, false, 5); - addLabel(thirdLeft0, thirdTop6, labelWidth, labelHeight, uiTextSize, "nNotch"); + addLabel(thirdLeft0, thirdTop8, labelWidth, labelHeight, uiTextSize, "Delay Count 1"); addTextKnob( - thirdLeft1, thirdTop6, labelWidth, labelHeight, uiTextSize, ID::nAdaptiveNotch, - Scales::nAdaptiveNotch, false, 0, 0); - addLabel(thirdLeft0, thirdTop7, labelWidth, labelHeight, uiTextSize, "Notch Mix"); + thirdLeft1, thirdTop8, labelWidth, labelHeight, uiTextSize, ID::allpassDelayCount1, + Scales::allpassDelayCount, false, 0, 1); + addLabel(thirdLeft0, thirdTop9, labelWidth, labelHeight, uiTextSize, "Delay Count 2"); addTextKnob( - thirdLeft1, thirdTop7, labelWidth, labelHeight, uiTextSize, ID::adaptiveNotchMix, - Scales::defaultScale, false, 5); - addLabel( - thirdLeft0, thirdTop8, labelWidth, labelHeight, uiTextSize, "Notch Narrowness"); - addTextKnob( - thirdLeft1, thirdTop8, labelWidth, labelHeight, uiTextSize, - ID::adaptiveNotchNarrowness, Scales::adaptiveNotchNarrowness, false, 5); - + thirdLeft1, thirdTop9, labelWidth, labelHeight, uiTextSize, ID::allpassDelayCount2, + Scales::allpassDelayCount, false, 0, 1); addLabel(thirdLeft0, thirdTop10, labelWidth, labelHeight, uiTextSize, "Feed 1"); addTextKnob( thirdLeft1, thirdTop10, labelWidth, labelHeight, uiTextSize, ID::allpassFeed1, diff --git a/DoubleLoopCymbal/source/parameter.cpp b/DoubleLoopCymbal/source/parameter.cpp index 8795659c..2867cc7e 100644 --- a/DoubleLoopCymbal/source/parameter.cpp +++ b/DoubleLoopCymbal/source/parameter.cpp @@ -45,11 +45,10 @@ DecibelScale Scales::halfClosedDensityHz(0.0, 80.0, true); DecibelScale Scales::delayTimeSecond(-100, -30, false); DecibelScale Scales::delayTimeModAmount(-40, 60, true); +UIntScale Scales::allpassDelayCount(nAllpass - 1); + DecibelScale Scales::cutoffFrequencyHz(0, 100, false); DecibelScale Scales::shelvingGain(-60, 0, true); -UIntScale Scales::nAdaptiveNotch(nNotch); -NegativeDecibelScale Scales::adaptiveNotchNarrowness(-60, 0, 1, false); - } // namespace Synth } // namespace Steinberg diff --git a/DoubleLoopCymbal/source/parameter.hpp b/DoubleLoopCymbal/source/parameter.hpp index 881f5037..23b69147 100644 --- a/DoubleLoopCymbal/source/parameter.hpp +++ b/DoubleLoopCymbal/source/parameter.hpp @@ -31,8 +31,7 @@ #include "../../common/value.hpp" #endif -constexpr size_t nAllpass = 16; -constexpr size_t nNotch = 1; +constexpr uint32_t nAllpass = 16; constexpr size_t nReservedParameter = 64; constexpr size_t nReservedGuiParameter = 16; @@ -79,7 +78,10 @@ enum ID { delayTimeShape, delayTimeBaseSecond, delayTimeRandomSecond, + delayTimeRatio, delayTimeModAmount, + allpassDelayCount1, + allpassDelayCount2, allpassFeed1, allpassFeed2, allpassMixSpike, @@ -90,10 +92,6 @@ enum ID { lowShelfFrequencyHz, lowShelfGain, - nAdaptiveNotch, - adaptiveNotchMix, - adaptiveNotchNarrowness, - reservedParameter0, reservedGuiParameter0 = reservedParameter0 + nReservedParameter, @@ -119,11 +117,10 @@ struct Scales { static SomeDSP::DecibelScale delayTimeSecond; static SomeDSP::DecibelScale delayTimeModAmount; + static SomeDSP::UIntScale allpassDelayCount; + static SomeDSP::DecibelScale cutoffFrequencyHz; static SomeDSP::DecibelScale shelvingGain; - - static SomeDSP::UIntScale nAdaptiveNotch; - static SomeDSP::NegativeDecibelScale adaptiveNotchNarrowness; }; struct GlobalParameter : public ParameterInterface { @@ -227,10 +224,19 @@ struct GlobalParameter : public ParameterInterface { value[ID::delayTimeRandomSecond] = std::make_unique( Scales::delayTimeSecond.invmap(0.001), Scales::delayTimeSecond, "delayTimeRandomSecond", Info::kCanAutomate); - + value[ID::delayTimeRatio] = std::make_unique( + Scales::semitone.invmap(0.0), Scales::semitone, "delayTimeRatio", + Info::kCanAutomate); value[ID::delayTimeModAmount] = std::make_unique( Scales::delayTimeModAmount.invmap(0.0), Scales::delayTimeModAmount, "delayTimeModAmount", Info::kCanAutomate); + + value[ID::allpassDelayCount1] = std::make_unique( + Scales::allpassDelayCount.getMax(), Scales::allpassDelayCount, "allpassDelayCount1", + Info::kCanAutomate); + value[ID::allpassDelayCount2] = std::make_unique( + Scales::allpassDelayCount.getMax(), Scales::allpassDelayCount, "allpassDelayCount2", + Info::kCanAutomate); value[ID::allpassFeed1] = std::make_unique( Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed1", Info::kCanAutomate); @@ -257,15 +263,6 @@ struct GlobalParameter : public ParameterInterface { Scales::shelvingGain.invmapDB(-1.0), Scales::shelvingGain, "lowShelfGain", Info::kCanAutomate); - value[ID::nAdaptiveNotch] = std::make_unique( - 0, Scales::nAdaptiveNotch, "nAdaptiveNotch", Info::kCanAutomate); - value[ID::adaptiveNotchMix] = std::make_unique( - Scales::defaultScale.invmap(0.25), Scales::defaultScale, "adaptiveNotchMix", - Info::kCanAutomate); - value[ID::adaptiveNotchNarrowness] = std::make_unique( - Scales::adaptiveNotchNarrowness.invmap(0.99), Scales::adaptiveNotchNarrowness, - "adaptiveNotchNarrowness", Info::kCanAutomate); - for (size_t idx = 0; idx < nReservedParameter; ++idx) { auto indexStr = std::to_string(idx); value[ID::reservedParameter0 + idx] = std::make_unique(