From f57bf7d79bf83053f85be7688ad34695c4cf8c38 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Mon, 9 Sep 2024 09:41:22 +0900 Subject: [PATCH] Update DoubleLoopCymbal (WIP) Adaptive notch check point. --- DoubleLoopCymbal/source/dsp/delay.hpp | 447 +++++++----------------- DoubleLoopCymbal/source/dsp/dspcore.cpp | 87 +++-- DoubleLoopCymbal/source/dsp/dspcore.hpp | 3 +- DoubleLoopCymbal/source/editor.cpp | 82 +++-- DoubleLoopCymbal/source/parameter.cpp | 4 +- DoubleLoopCymbal/source/parameter.hpp | 50 ++- common/dsp/scale.hpp | 53 ++- 7 files changed, 323 insertions(+), 403 deletions(-) diff --git a/DoubleLoopCymbal/source/dsp/delay.hpp b/DoubleLoopCymbal/source/dsp/delay.hpp index 47b1158a..db0ea4da 100644 --- a/DoubleLoopCymbal/source/dsp/delay.hpp +++ b/DoubleLoopCymbal/source/dsp/delay.hpp @@ -29,11 +29,10 @@ namespace SomeDSP { template class ExpDecay { -private: +public: Sample value = 0; Sample alpha = 0; -public: void setTime(Sample decayTimeInSamples, bool sustain = false) { constexpr auto eps = Sample(std::numeric_limits::epsilon()); @@ -41,60 +40,158 @@ template class ExpDecay { } void reset() { value = 0; } - void noteOn(Sample gain = Sample(1)) { value = gain; } + void trigger(Sample gain = Sample(1)) { value = gain; } Sample process() { return value *= alpha; } }; -template class ExpRise { +template class ExpDSREnvelope { +public: + enum class State { decay, release }; + private: - static constexpr auto eps = Sample(std::numeric_limits::epsilon()); - Sample value = eps; - Sample alpha = 0; + Sample value = 0; + Sample alphaD = 0; + Sample alphaR = 0; + Sample offset = 0; + State state = State::release; public: - void setTime(Sample decayTimeInSamples) + void setTime(Sample decayTimeInSamples, Sample releaseTimeInSamples) + { + constexpr auto eps = Sample(std::numeric_limits::epsilon()); + alphaD = std::pow(eps, Sample(1) / decayTimeInSamples); + alphaR = std::pow(eps, Sample(1) / releaseTimeInSamples); + } + + void reset() + { + value = 0; + alphaD = 0; + alphaR = 0; + offset = 0; + state = State::release; + } + + void trigger(Sample sustainLevel) { - alpha = std::pow(Sample(1) / eps, Sample(1) / decayTimeInSamples); + state = State::decay; + value = Sample(1) - sustainLevel; + offset = sustainLevel; } - void reset() { value = eps; } - void noteOn(Sample gain = eps) { value = gain; } - Sample process() { return value = std::min(value * alpha, Sample(1)); } + void release() + { + state = State::release; + offset = 0; + } + + Sample process() + { + if (state == State::decay) { + value *= alphaD; + return offset + value; + } + return value *= alphaR; + } }; -template class LinearDecay { +template class TransitionReleaseSmoother { private: - Sample decaySamples = Sample(1); - Sample gain = Sample(1); - int counter = 0; + Sample v0 = 0; + Sample decay = 0; public: - void setTime(Sample decayTimeInSamples) + // decaySamples = sampleRate * seconds. + void setup(Sample decaySamples) { - constexpr auto eps = Sample(std::numeric_limits::epsilon()); - decaySamples = std::max(Sample(1), decayTimeInSamples); + decay = std::pow(std::numeric_limits::epsilon(), Sample(1) / decaySamples); } + void reset() { v0 = 0; } + + void prepare(Sample value, Sample decaySamples) + { + v0 += value; + decay = std::pow(std::numeric_limits::epsilon(), Sample(1) / decaySamples); + } + + Sample process() { return v0 *= decay; } +}; + +template class ExpADEnvelope { +private: + static constexpr Sample epsilon = std::numeric_limits::epsilon(); + Sample targetGain = 0; + Sample velocity = 0; + Sample gain = Sample(1); + Sample smoo = Sample(1); + Sample valueA = 0; + Sample alphaA = 0; + Sample valueD = 0; + Sample alphaD = 0; + +public: + bool isTerminated() { return valueD <= Sample(1e-3); } + + void setup(Sample smoothingKp) { smoo = smoothingKp; } + void reset() { - decaySamples = Sample(1); + targetGain = 0; gain = Sample(1); - counter = 0; + valueA = 0; + alphaA = 0; + valueD = 0; + alphaD = 0; } - void noteOn(Sample gain_ = Sample(1)) + enum class NormalizationType { peak, energy }; + + void update( + Sample sampleRate, + Sample peakSeconds, + Sample releaseSeconds, + Sample peakGain, + NormalizationType normalization = NormalizationType::energy) { - gain = gain_ / decaySamples; - counter = int(decaySamples); + const auto decaySeconds = releaseSeconds - std::log(epsilon) * peakSeconds; + const auto d_ = std::log(epsilon) / decaySeconds; + const auto x_ = d_ * peakSeconds; + const auto a_ = Sample(utl::LambertW(-1, x_ * std::exp(x_))) / peakSeconds - d_; + + const auto attackSeconds = -std::log(epsilon) / std::log(-a_); + alphaA = std::exp(a_ / sampleRate); + alphaD = std::exp(d_ / sampleRate); + + if (normalization == NormalizationType::energy) { + // `area` is obtained by solving `integrate((1-%e^(-a*t))*%e^(-d*t), t, 0, +inf);`. + const auto area = -a_ / (d_ * (d_ + a_)); + targetGain = Sample(1e-1) * peakGain / area; + } else { // `normalization == NormalizationType::peak`. + targetGain + = peakGain / (-std::expm1(a_ * peakSeconds) * std::exp(d_ * peakSeconds)); + } } - void noteOff() { counter = 0; } + void trigger( + Sample sampleRate, + Sample peakSeconds, + Sample releaseSeconds, + Sample peakGain, + Sample velocity_) + { + velocity = velocity_; + valueA = Sample(1); + valueD = Sample(1); + update(sampleRate, peakSeconds, releaseSeconds, peakGain); + } Sample process() { - if (counter <= 0) return 0; - --counter; - return gain * Sample(counter); + gain += smoo * (targetGain - gain); + valueA *= alphaA; + valueD *= alphaD; + return velocity * gain * (Sample(1) - (valueA)) * valueD; } }; @@ -151,63 +248,9 @@ template class Delay { } }; -/** -Unused. This is a drop in replacement of `Delay`, but doubles the CPU load. - -- Sustain of the sound becomes shorter. Perhaps second allpass loop is required. -- Rotating `phase` at right frequency makes better sound. It adds artifact due to AM. -- `inputRatio` is better to be controled by user. -*/ -template class FDN2 { -public: - static constexpr size_t size = 2; - - Sample phase = 0; - std::array buffer; - std::array, size> delay; - - void setup(Sample maxTimeSamples) - { - for (auto &x : delay) x.setup(maxTimeSamples); - } - - void reset() - { - buffer.fill({}); - for (auto &x : delay) x.reset(); - } - - Sample process(Sample input, Sample timeInSamples) - { - // Fixed parameters. [a, b] is the range of the value. - constexpr auto phaseRatio = Sample(1) / Sample(3); // [positive small, +inf]. - constexpr auto feedback = Sample(1); // [-1, 1]. - constexpr auto timeRatio = Sample(4) / Sample(3); // [0, +inf]. - constexpr auto inputRatio = Sample(0.5); // [0, 1]. - - constexpr auto twopi = Sample(2) * std::numbers::pi_v; - const auto cs = std::cos(twopi * phase); - const auto sn = std::sin(twopi * phase); - - phase += phaseRatio / timeInSamples; - phase -= std::floor(phase); - - const auto sig0 = feedback * (cs * buffer[0] - sn * buffer[1]); - const auto sig1 = feedback * (sn * buffer[0] + cs * buffer[1]); - - constexpr auto g1 = inputRatio; - constexpr auto g2 = Sample(1) - inputRatio; - buffer[0] = delay[0].process(g1 * input + sig0, timeInSamples); - buffer[1] = delay[1].process(g2 * input + sig1, timeInSamples * timeRatio); - - return buffer[0] + buffer[1]; - } -}; - -// Unused. `SerialAllpass` became unstable when used. -template class NyquistLowpass { +template class Highpass2 { private: - static constexpr Sample g = Sample(15915.494288237813); // tan(0.49998 * pi). + // static constexpr Sample g = Sample(0.02); // ~= tan(pi * 300 / 48000). static constexpr Sample k = Sample(0.7071067811865476); // 2 / sqrt(2). Sample ic1eq = 0; @@ -220,34 +263,11 @@ template class NyquistLowpass { ic2eq = 0; } - Sample process(Sample input) - { - const auto v1 = (ic1eq + g * (input - ic2eq)) / (1 + g * (g + k)); - const auto v2 = ic2eq + g * v1; - ic1eq = Sample(2) * v1 - ic1eq; - ic2eq = Sample(2) * v2 - ic2eq; - return v2; - } -}; - -// Unused. `SerialAllpass` became unstable when used. -template class FixedHighpass { -private: - static constexpr Sample g = Sample(0.02); // ~= tan(pi * 300 / 48000). - static constexpr Sample k = Sample(0.7071067811865476); // 2 / sqrt(2). - - Sample ic1eq = 0; - Sample ic2eq = 0; - -public: - void reset() - { - ic1eq = 0; - ic2eq = 0; - } - - Sample process(Sample input) + Sample process(Sample input, Sample cutoffNormalized) { + const auto g = std::tan( + std::numbers::pi_v + * std::clamp(cutoffNormalized, Sample(0.00001), Sample(0.49998))); const auto v1 = (ic1eq + g * (input - ic2eq)) / (1 + g * (g + k)); const auto v2 = ic2eq + g * v1; ic1eq = Sample(2) * v1 - ic1eq; @@ -284,119 +304,6 @@ template class EmaLowShelf { } }; -// Unused. -template class AdaptiveNotchAM { -public: - static constexpr Sample mu = Sample(2) / Sample(256); - Sample alpha = Sample(-2); - - Sample x1 = 0; - Sample x2 = 0; - Sample y1 = 0; - Sample y2 = 0; - - void reset() - { - alpha = Sample(-2); // 0 Hz as initial guess. - - x1 = 0; - x2 = 0; - y1 = 0; - y2 = 0; - } - - Sample process(Sample input, Sample narrowness) - { - const auto a1 = narrowness * alpha; - const auto a2 = narrowness * narrowness; - const 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 y0 = std::clamp(input + alpha * x1 + x2 - a1 * y1 - a2 * y2, -clip, clip); - const auto s0 = x1 * (Sample(1) - narrowness * y0); - constexpr auto bound = Sample(2); - alpha += Sample(0.0625) * (std::clamp(alpha - mu * y0 * s0, -bound, bound) - alpha); - - x2 = x1; - x1 = input; - y2 = y1; - y1 = y0; - - return y0 * gain; - } -}; - -// Unused. Unstable when put in feedback loop. -template class AdaptiveNotchAP1 { -public: - Sample alpha = Sample(-2); - - Sample v1 = 0; - Sample v2 = 0; - Sample q1 = 0; - Sample r1 = 0; - - void reset() - { - alpha = Sample(-2); // 0 Hz as initial guess. - - v1 = 0; - v2 = 0; - q1 = 0; - r1 = 0; - } - - inline Sample approxAtoK(Sample x) - { - constexpr std::array b{ - Sample(0.24324073816096012), Sample(-0.2162512785673542), - Sample(0.0473854827531673)}; - constexpr std::array a{ - Sample(-0.9569399371569998), Sample(0.23899241566503945), - Sample(-0.005191170339007643)}; - - const auto z = std::abs(x); - const auto y - = z * (b[0] + z * (b[1] + z * b[2])) / (1 + z * (a[0] + z * (a[1] + z * a[2]))); - return std::copysign(y, x); - } - - Sample process(Sample input, Sample narrowness) - { - const auto a1 = narrowness * alpha; - const auto a2 = narrowness * narrowness; - const auto gain = alpha >= 0 ? (Sample(1) + a1 + a2) / (Sample(2) + alpha) - : (Sample(1) - a1 + a2) / (Sample(2) - alpha); - - constexpr auto clip = Sample(1024); - 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; - const auto alphaCpz = y0 * s0; - - const auto omega_a = std::numbers::pi_v - std::acos(alpha / 2); - const auto t = std::tan(omega_a / Sample(2)); - const auto k_ap = (t - Sample(1)) / (t + Sample(1)); - // const auto k_ap = approxAtoK(alpha); - r1 = k_ap * (x0 - r1) + q1; - q1 = x0; - const auto alphaAM = std::min(std::abs(y0), Sample(1)) * x0 * r1; - - constexpr auto bound = Sample(2); - constexpr auto mu = Sample(1) / Sample(256); - alpha = std::clamp(alpha - mu * alphaAM, -bound, bound); - // alpha = std::clamp(alpha - mu * (alphaCpz + alphaAM), -bound, bound); - - v2 = v1; - v1 = v0; - - return y0 * gain; - } -}; - template class AdaptiveNotchCPZ { public: static constexpr Sample mu = Sample(2) / Sample(1024); @@ -513,99 +420,13 @@ template class SerialAl } }; -template class TransitionReleaseSmoother { -private: - Sample v0 = 0; - Sample decay = 0; - -public: - // decaySamples = sampleRate * seconds. - void setup(Sample decaySamples) - { - decay = std::pow(std::numeric_limits::epsilon(), Sample(1) / decaySamples); - } - - void reset() { v0 = 0; } - - void prepare(Sample value, Sample decaySamples) - { - v0 += value; - decay = std::pow(std::numeric_limits::epsilon(), Sample(1) / decaySamples); - } - - Sample process() { return v0 *= decay; } -}; - -template class ExpADEnvelope { -private: - static constexpr Sample epsilon = std::numeric_limits::epsilon(); - Sample targetGain = 0; - Sample gain = Sample(1); - Sample smoo = Sample(1); - Sample valueA = 0; - Sample alphaA = 0; - Sample valueD = 0; - Sample alphaD = 0; - -public: - bool isTerminated() { return valueD <= Sample(1e-3); } - - void setup(Sample smoothingKp) { smoo = smoothingKp; } - - void reset() - { - targetGain = 0; - gain = Sample(1); - valueA = 0; - alphaA = 0; - valueD = 0; - alphaD = 0; - } - - void - update(Sample sampleRate, Sample peakSeconds, Sample releaseSeconds, Sample peakGain) - { - const auto decaySeconds = releaseSeconds - std::log(epsilon) * peakSeconds; - const auto d_ = std::log(epsilon) / decaySeconds; - const auto x_ = d_ * peakSeconds; - const auto a_ = Sample(utl::LambertW(-1, x_ * std::exp(x_))) / peakSeconds - d_; - - const auto attackSeconds = -std::log(epsilon) / std::log(-a_); - alphaA = std::exp(a_ / sampleRate); - alphaD = std::exp(d_ / sampleRate); - - // `area` is obtained by solving `integrate((1-%e^(-a*t))*%e^(-d*t), t, 0, +inf);`. - const auto area = -a_ / (d_ * (d_ + a_)); - - // targetGain = peakGain - // / ((Sample(1) - std::exp(a_ * peakSeconds)) * std::exp(d_ * peakSeconds)); - targetGain = Sample(1e-3) * peakGain / area; - } - - void - noteOn(Sample sampleRate, Sample peakSeconds, Sample releaseSeconds, Sample peakGain) - { - valueA = Sample(1); - valueD = Sample(1); - update(sampleRate, peakSeconds, releaseSeconds, peakGain); - } - - Sample process() - { - gain += smoo * (targetGain - gain); - valueA *= alphaA; - valueD *= alphaD; - return gain * (Sample(1) - (valueA)) * valueD; - } -}; - template class HalfClosedNoise { private: Sample phase = 0; Sample gain = Sample(1); Sample decay = 0; - FixedHighpass highpass; + Highpass2 highpass; public: void reset() @@ -624,7 +445,7 @@ template class HalfClosedNoise { // `density` is inverse of average samples between impulses. // `randomGain` is in [0, 1]. - Sample process(Sample density, Sample randomGain, pcg64 &rng) + Sample process(Sample density, Sample randomGain, Sample highpassNormalized, pcg64 &rng) { std::uniform_real_distribution jitter(Sample(0), Sample(1)); phase += jitter(rng) * density; @@ -639,7 +460,7 @@ template class HalfClosedNoise { std::uniform_real_distribution distNoise(-Sample(1), Sample(1)); const auto noise = distNoise(rng); - return highpass.process(noise * noise * noise * gain); + return highpass.process(noise * noise * noise * gain, highpassNormalized); } }; diff --git a/DoubleLoopCymbal/source/dsp/dspcore.cpp b/DoubleLoopCymbal/source/dsp/dspcore.cpp index 20a6c525..31e9a811 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.cpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.cpp @@ -159,7 +159,6 @@ void DSPCore::setup(double sampleRate) releaseSmoother.setup(double(2) * upRate); envelopeClose.setup(EMAFilter::secondToP(upRate, double(0.004))); - for (auto &x : halfClosedNoise) x.setDecay(double(0.1) * upRate); const auto maxDelayTimeSamples = upRate * 2 * Scales::delayTimeSecond.getMax(); for (auto &x : serialAllpass1) x.setup(maxDelayTimeSamples); @@ -182,7 +181,8 @@ void DSPCore::setup(double sampleRate) \ externalInputGain.METHOD(pv[ID::externalInputGain]->getDouble()); \ halfClosedGain.METHOD(pv[ID::halfClosedGain]->getDouble()); \ - halfClosedDensity.METHOD(pv[ID::halfClosedDensity]->getDouble() / upRate); \ + halfClosedDensity.METHOD(pv[ID::halfClosedDensityHz]->getDouble() / upRate); \ + halfClosedHighpassCutoff.METHOD(pv[ID::halfClosedHighpassHz]->getDouble() / upRate); \ delayTimeModAmount.METHOD( \ pv[ID::delayTimeModAmount]->getDouble() * upRate / double(48000)); \ allpassFeed1.METHOD( \ @@ -206,15 +206,23 @@ void DSPCore::setup(double sampleRate) outputGain.METHOD(gain); \ \ envelopeNoise.setTime(pv[ID::noiseDecaySeconds]->getDouble() * upRate); \ - envelopeHalfClosed.setTime(pv[ID::halfClosedDecaySeconds]->getDouble() * upRate); \ - envelopeClose.update( \ - upRate, pv[ID::closeAttackSeconds]->getDouble(), closeReleaseSecond, \ - pv[ID::closeGain]->getDouble()); \ + \ + const auto halfCloseDecaySecond = pv[ID::halfCloseDecaySecond]->getDouble(); \ + envelopeHalfClosed.setTime(halfCloseDecaySecond *upRate, closeReleaseSecond *upRate); \ + \ if (!pv[ID::release]->getInt() && noteStack.empty()) { \ envelopeRelease.setTime( \ double(8) * pv[ID::closeAttackSeconds]->getDouble() * upRate); \ } \ \ + envelopeClose.update( \ + upRate, pv[ID::closeAttackSeconds]->getDouble(), closeReleaseSecond, \ + pv[ID::closeGain]->getDouble()); \ + \ + for (auto &x : halfClosedNoise) { \ + x.setDecay(pv[ID::halfClosedPulseSecond]->getDouble() * upRate); \ + } \ + \ updateDelayTime(); void DSPCore::updateUpRate() @@ -261,8 +269,8 @@ void DSPCore::reset() impulse = 0; releaseSmoother.reset(); envelopeNoise.reset(); - envelopeRelease.reset(); envelopeHalfClosed.reset(); + envelopeRelease.reset(); envelopeClose.reset(); for (auto &x : halfClosedNoise) x.reset(); feedbackBuffer1.fill(double(0)); @@ -293,15 +301,18 @@ void DSPCore::setParameters() std::array DSPCore::processFrame(const std::array &externalInput) { + using ID = ParameterID::ID; + const auto &pv = param.value; + const auto envRelease = envelopeRelease.process(); - const auto terminationGain = envRelease < double(1e-3) ? double(0) : double(1); const auto extGain = externalInputGain.process(); - const auto hcGain = halfClosedGain.process(); + const auto hcGain = halfClosedGain.process() * envelopeHalfClosed.process(); const auto hcDensity = halfClosedDensity.process(); + const auto hcCutoff = halfClosedHighpassCutoff.process(); auto timeModAmt = delayTimeModAmount.process(); - timeModAmt += (double(1) - envRelease) * (double(10) * upRate / double(48000)); + // timeModAmt += (double(1) - envRelease) * (double(10) * upRate / double(48000)); auto apGain1 = allpassFeed1.process(); auto apGain2 = allpassFeed2.process(); @@ -323,14 +334,12 @@ std::array DSPCore::processFrame(const std::array &externa std::uniform_real_distribution dist{double(-1), double(1)}; auto noiseEnv = releaseSmoother.process() + envelopeNoise.process(); - if (!param.value[ParameterID::release]->getInt() && noteStack.empty()) { - // TODO: simplify condition. + if (!pv[ID::release]->getInt() && noteStack.empty()) { noiseEnv += envelopeClose.process(); } std::array excitation{ - -terminationGain * apGain1 * feedbackBuffer1[0], - -terminationGain * apGain1 * feedbackBuffer1[1]}; + -apGain1 * feedbackBuffer1[0], -apGain1 * feedbackBuffer1[1]}; if (impulse != 0) { excitation[0] += impulse; excitation[1] += impulse; @@ -340,11 +349,11 @@ std::array DSPCore::processFrame(const std::array &externa excitation[0] += noiseEnv * ipow(dist(noiseRng)); excitation[1] += noiseEnv * ipow(dist(noiseRng)); - const auto hcEnv = envelopeHalfClosed.process(); - const auto density = hcDensity * std::exp2(double(8) - double(8) * hcEnv); - const auto gn = hcGain * velocity * hcEnv * hcEnv; - excitation[0] += gn * halfClosedNoise[0].process(density, double(1), noiseRng); - excitation[1] += gn * halfClosedNoise[1].process(density, double(1), noiseRng); + const auto g = hcGain * velocity; + excitation[0] + += g * halfClosedNoise[0].process(hcDensity, double(1), hcCutoff, noiseRng); + excitation[1] + += g * halfClosedNoise[1].process(hcDensity, double(1), hcCutoff, noiseRng); } if (useExternalInput) { @@ -377,11 +386,11 @@ std::array DSPCore::processFrame(const std::array &externa = std::lerp(serialAllpass2[1].sum(apMixSign), feedbackBuffer2[1], apMixSpike) * normalizeGain; feedbackBuffer2[0] = serialAllpass2[0].process( - ap1Out0 - terminationGain * apGain2 * feedbackBuffer2[0], hsCut, hsGain, lsCut, - lsGain, apGain2, pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); + ap1Out0 - apGain2 * feedbackBuffer2[0], hsCut, hsGain, lsCut, lsGain, apGain2, + pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); feedbackBuffer2[1] = serialAllpass2[1].process( - ap1Out1 - terminationGain * apGain2 * feedbackBuffer2[1], hsCut, hsGain, lsCut, - lsGain, apGain2, pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); + ap1Out1 - apGain2 * feedbackBuffer2[1], hsCut, hsGain, lsCut, lsGain, apGain2, + pitchRatio, timeModAmt, nNotch, ntMix, ntNarrowness); constexpr auto eps = std::numeric_limits::epsilon(); if (balance < -eps) { @@ -447,8 +456,6 @@ void DSPCore::noteOn(NoteInfo &info) constexpr auto eps = std::numeric_limits::epsilon(); - noteStack.push_back(info); - noteNumber = info.noteNumber; auto notePitch = calcNotePitch(info.noteNumber); interpPitch.push(notePitch); @@ -457,24 +464,30 @@ void DSPCore::noteOn(NoteInfo &info) if (pv[ID::resetSeedAtNoteOn]->getInt()) noiseRng.seed(pv[ID::seed]->getInt()); - // if (envelopeClose.isTerminated() && !pv[ID::release]->getInt()) { - const auto lossGain = pv[ID::lossGain]->getDouble(); + const auto lossGain = !pv[ID::release]->getInt() && noteStack.empty() + ? envelopeRelease.value + : pv[ID::lossGain]->getDouble(); for (auto &x : serialAllpass1) x.applyGain(lossGain); for (auto &x : serialAllpass2) x.applyGain(lossGain); - // } - const auto oscGain = velocity * pv[ID::noiseGain]->getDouble(); releaseSmoother.prepare( double(0.1) * envelopeNoise.process(), upRate * pv[ID::noiseDecaySeconds]->getDouble()); + + const auto oscGain = velocity * pv[ID::noiseGain]->getDouble(); impulse = oscGain; - envelopeNoise.noteOn(oscGain); - envelopeHalfClosed.noteOn(); - envelopeRelease.noteOn(double(1)); - envelopeRelease.setTime(1, true); - envelopeClose.noteOn( + envelopeNoise.trigger(oscGain); + + envelopeHalfClosed.trigger(pv[ID::halfCloseSustainLevel]->getDouble()); + + envelopeRelease.trigger(double(1)); + envelopeRelease.setTime(double(1), true); + + envelopeClose.trigger( upRate, pv[ID::closeAttackSeconds]->getDouble(), closeReleaseSecond, - velocity * pv[ID::closeGain]->getDouble()); + pv[ID::closeGain]->getDouble(), velocity); + + noteStack.push_back(info); } void DSPCore::noteOff(int_fast32_t noteId) @@ -492,10 +505,10 @@ void DSPCore::noteOff(int_fast32_t noteId) noteNumber = noteStack.back().noteNumber; interpPitch.push(calcNotePitch(noteNumber)); } else { + velocity = 0; + envelopeHalfClosed.release(); if (!pv[ID::release]->getInt()) { envelopeRelease.setTime(releaseTimeSecond * upRate); - } else { - envelopeHalfClosed.noteOff(); } } } diff --git a/DoubleLoopCymbal/source/dsp/dspcore.hpp b/DoubleLoopCymbal/source/dsp/dspcore.hpp index 6c2ad6ee..8e3254ed 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.hpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.hpp @@ -119,6 +119,7 @@ class DSPCore { ExpSmoother externalInputGain; ExpSmoother halfClosedGain; ExpSmoother halfClosedDensity; + ExpSmoother halfClosedHighpassCutoff; ExpSmoother delayTimeModAmount; ExpSmoother allpassFeed1; ExpSmoother allpassFeed2; @@ -141,8 +142,8 @@ class DSPCore { double impulse = 0; TransitionReleaseSmoother releaseSmoother; ExpDecay envelopeNoise; + ExpDSREnvelope envelopeHalfClosed; ExpDecay envelopeRelease; - LinearDecay envelopeHalfClosed; ExpADEnvelope envelopeClose; std::array, 2> halfClosedNoise; std::array feedbackBuffer1{}; diff --git a/DoubleLoopCymbal/source/editor.cpp b/DoubleLoopCymbal/source/editor.cpp index 1d1905ec..f43bad5e 100644 --- a/DoubleLoopCymbal/source/editor.cpp +++ b/DoubleLoopCymbal/source/editor.cpp @@ -198,6 +198,8 @@ bool Editor::prepareUI() constexpr auto impactTop11 = impactTop0 + 11 * labelY; constexpr auto impactTop12 = impactTop0 + 12 * labelY; constexpr auto impactTop13 = impactTop0 + 13 * labelY; + constexpr auto impactTop14 = impactTop0 + 14 * labelY; + constexpr auto impactTop15 = impactTop0 + 15 * labelY; constexpr auto impactLeft0 = left4; constexpr auto impactLeft1 = impactLeft0 + labelWidth + 2 * margin; addGroupLabel( @@ -215,7 +217,7 @@ bool Editor::prepareUI() impactLeft0, impactTop2, labelWidth, labelHeight, uiTextSize, "Noise Gain [dB]"); addTextKnob( impactLeft1, impactTop2, labelWidth, labelHeight, uiTextSize, ID::noiseGain, - Scales::gain, true, 5); + Scales::halfClosedGain, true, 5); addLabel( impactLeft0, impactTop3, labelWidth, labelHeight, uiTextSize, "Noise Decay [s]"); addTextKnob( @@ -230,31 +232,50 @@ bool Editor::prepareUI() Scales::halfClosedGain, true, 5); addLabel( impactLeft0, impactTop6, labelWidth, labelHeight, uiTextSize, - "Half Closed Density [Hz]"); + "Half Closed Decay [s]"); addTextKnob( - impactLeft1, impactTop6, labelWidth, labelHeight, uiTextSize, ID::halfClosedDensity, - Scales::halfClosedDensity, false, 5); + impactLeft1, impactTop6, labelWidth, labelHeight, uiTextSize, + ID::halfCloseDecaySecond, Scales::noiseDecaySeconds, false, 5); addLabel( impactLeft0, impactTop7, labelWidth, labelHeight, uiTextSize, - "Half Closed Decay [s]"); + "Half Closed Sustain [dB]"); addTextKnob( impactLeft1, impactTop7, labelWidth, labelHeight, uiTextSize, - ID::halfClosedDecaySeconds, Scales::noiseDecaySeconds, false, 5); + ID::halfCloseSustainLevel, Scales::halfClosedGain, true, 5); + addLabel( + impactLeft0, impactTop8, labelWidth, labelHeight, uiTextSize, + "Half Closed Pulse Duration [s]"); + addTextKnob( + impactLeft1, impactTop8, labelWidth, labelHeight, uiTextSize, + ID::halfClosedPulseSecond, Scales::noiseDecaySeconds, false, 5); + addLabel( + impactLeft0, impactTop9, labelWidth, labelHeight, uiTextSize, + "Half Closed Density [Hz]"); + addTextKnob( + impactLeft1, impactTop9, labelWidth, labelHeight, uiTextSize, ID::halfClosedDensityHz, + Scales::halfClosedDensityHz, false, 5); + addLabel( + impactLeft0, impactTop10, labelWidth, labelHeight, uiTextSize, + "Half Closed Highpass [Hz]"); + addTextKnob( + impactLeft1, impactTop10, labelWidth, labelHeight, uiTextSize, + ID::halfClosedHighpassHz, Scales::halfClosedDensityHz, false, 5); addLabel( - impactLeft0, impactTop10, labelWidth, labelHeight, uiTextSize, "Close Gain [dB]"); + impactLeft0, impactTop12, labelWidth, labelHeight, uiTextSize, "Close Gain [dB]"); addTextKnob( - impactLeft1, impactTop10, labelWidth, labelHeight, uiTextSize, ID::closeGain, - Scales::gain, true, 5); + impactLeft1, impactTop12, labelWidth, labelHeight, uiTextSize, ID::closeGain, + Scales::halfClosedGain, true, 5); addLabel( - impactLeft0, impactTop11, labelWidth, labelHeight, uiTextSize, "Close Attack [s]"); + impactLeft0, impactTop13, labelWidth, labelHeight, uiTextSize, "Close Attack [s]"); addTextKnob( - impactLeft1, impactTop11, labelWidth, labelHeight, uiTextSize, ID::closeAttackSeconds, + impactLeft1, impactTop13, labelWidth, labelHeight, uiTextSize, ID::closeAttackSeconds, Scales::noiseDecaySeconds, false, 5); + addLabel( - impactLeft0, impactTop12, labelWidth, labelHeight, uiTextSize, "Loss Gain [dB]"); + impactLeft0, impactTop14, labelWidth, labelHeight, uiTextSize, "Loss Gain [dB]"); addTextKnob( - impactLeft1, impactTop12, labelWidth, labelHeight, uiTextSize, ID::lossGain, + impactLeft1, impactTop14, labelWidth, labelHeight, uiTextSize, ID::lossGain, Scales::halfClosedGain, true, 5); // TODO: Delays. @@ -263,40 +284,39 @@ bool Editor::prepareUI() constexpr auto filterTop2 = filterTop0 + 2 * labelY; constexpr auto filterTop3 = filterTop0 + 3 * labelY; constexpr auto filterTop4 = filterTop0 + 4 * labelY; - constexpr auto filterTop5 = filterTop0 + 5 * labelY; - constexpr auto filterTop6 = filterTop0 + 6 * labelY; - constexpr auto filterTop7 = filterTop0 + 7 * labelY; - constexpr auto filterTop8 = filterTop0 + 8 * labelY; - constexpr auto filterTop9 = filterTop0 + 9 * labelY; - constexpr auto filterTop10 = filterTop0 + 10 * labelY; - constexpr auto filterTop11 = filterTop0 + 11 * labelY; constexpr auto filterLeft0 = left8; constexpr auto filterLeft1 = filterLeft0 + labelWidth + 2 * margin; addGroupLabel( filterLeft0, filterTop0, groupLabelWidth, labelHeight, uiTextSize, "Filter"); addLabel( - filterLeft0, filterTop8, labelWidth, labelHeight, uiTextSize, + filterLeft0, filterTop1, labelWidth, labelHeight, uiTextSize, "High Shelf Cutoff [Hz]"); addTextKnob( - filterLeft1, filterTop8, labelWidth, labelHeight, uiTextSize, + filterLeft1, filterTop1, labelWidth, labelHeight, uiTextSize, ID::highShelfFrequencyHz, Scales::cutoffFrequencyHz, false, 5); addLabel( - filterLeft0, filterTop9, labelWidth, labelHeight, uiTextSize, "High Shelf Gain [dB]"); - addTextKnob( - filterLeft1, filterTop9, labelWidth, labelHeight, uiTextSize, ID::highShelfGain, + filterLeft0, filterTop2, labelWidth, labelHeight, uiTextSize, "High Shelf Gain [dB]"); + auto highShelfGainTextKnob = addTextKnob( + filterLeft1, filterTop2, labelWidth, labelHeight, uiTextSize, ID::highShelfGain, Scales::shelvingGain, true, 5); + if (highShelfGainTextKnob) { + highShelfGainTextKnob->lowSensitivity = 1.0 / 30000.0; + } addLabel( - filterLeft0, filterTop10, labelWidth, labelHeight, uiTextSize, + filterLeft0, filterTop3, labelWidth, labelHeight, uiTextSize, "Low Shelf Cutoff [Hz]"); addTextKnob( - filterLeft1, filterTop10, labelWidth, labelHeight, uiTextSize, - ID::lowShelfFrequencyHz, Scales::cutoffFrequencyHz, false, 5); + filterLeft1, filterTop3, labelWidth, labelHeight, uiTextSize, ID::lowShelfFrequencyHz, + Scales::cutoffFrequencyHz, false, 5); addLabel( - filterLeft0, filterTop11, labelWidth, labelHeight, uiTextSize, "Low Shelf Gain [dB]"); - addTextKnob( - filterLeft1, filterTop11, labelWidth, labelHeight, uiTextSize, ID::lowShelfGain, + filterLeft0, filterTop4, labelWidth, labelHeight, uiTextSize, "Low Shelf Gain [dB]"); + auto lowShelfGainTextKnob = addTextKnob( + filterLeft1, filterTop4, labelWidth, labelHeight, uiTextSize, ID::lowShelfGain, Scales::shelvingGain, true, 5); + if (highShelfGainTextKnob) { + lowShelfGainTextKnob->lowSensitivity = 1.0 / 30000.0; + } // TODO: Delays. constexpr auto thirdTop0 = top0 + 0 * labelY; diff --git a/DoubleLoopCymbal/source/parameter.cpp b/DoubleLoopCymbal/source/parameter.cpp index 7fd2c6dc..8795659c 100644 --- a/DoubleLoopCymbal/source/parameter.cpp +++ b/DoubleLoopCymbal/source/parameter.cpp @@ -41,8 +41,8 @@ DecibelScale Scales::noteSlideTimeSecond(-100.0, 40.0, true); DecibelScale Scales::noiseDecaySeconds(-100, 40, false); DecibelScale Scales::halfClosedGain(-100.0, 0.0, true); -DecibelScale Scales::halfClosedDensity(0.0, 80.0, true); -DecibelScale Scales::delayTimeSecond(-100, -20, false); +DecibelScale Scales::halfClosedDensityHz(0.0, 80.0, true); +DecibelScale Scales::delayTimeSecond(-100, -30, false); DecibelScale Scales::delayTimeModAmount(-40, 60, true); DecibelScale Scales::cutoffFrequencyHz(0, 100, false); diff --git a/DoubleLoopCymbal/source/parameter.hpp b/DoubleLoopCymbal/source/parameter.hpp index 3d65a174..881f5037 100644 --- a/DoubleLoopCymbal/source/parameter.hpp +++ b/DoubleLoopCymbal/source/parameter.hpp @@ -32,7 +32,7 @@ #endif constexpr size_t nAllpass = 16; -constexpr size_t nNotch = 4; +constexpr size_t nNotch = 1; constexpr size_t nReservedParameter = 64; constexpr size_t nReservedGuiParameter = 16; @@ -66,8 +66,11 @@ enum ID { noiseDecaySeconds, halfClosedGain, - halfClosedDensity, - halfClosedDecaySeconds, + halfCloseDecaySecond, + halfCloseSustainLevel, + halfClosedPulseSecond, + halfClosedDensityHz, + halfClosedHighpassHz, closeGain, closeAttackSeconds, @@ -112,7 +115,7 @@ struct Scales { static SomeDSP::DecibelScale noiseDecaySeconds; static SomeDSP::DecibelScale halfClosedGain; - static SomeDSP::DecibelScale halfClosedDensity; + static SomeDSP::DecibelScale halfClosedDensityHz; static SomeDSP::DecibelScale delayTimeSecond; static SomeDSP::DecibelScale delayTimeModAmount; @@ -135,6 +138,7 @@ struct GlobalParameter : public ParameterInterface { using LinearValue = DoubleValue>; using DecibelValue = DoubleValue>; using NegativeDecibelValue = DoubleValue>; + using NegativeDoubleExpValue = DoubleValue>; value[ID::bypass] = std::make_unique( 0, Scales::boolScale, "bypass", Info::kCanAutomate | Info::kIsBypass); @@ -178,28 +182,40 @@ struct GlobalParameter : public ParameterInterface { value[ID::seed] = std::make_unique(0, Scales::seed, "seed", Info::kCanAutomate); value[ID::noiseGain] = std::make_unique( - Scales::gain.invmap(0.25), Scales::gain, "noiseGain", Info::kCanAutomate); + Scales::halfClosedGain.invmap(0.25), Scales::halfClosedGain, "noiseGain", + Info::kCanAutomate); value[ID::noiseDecaySeconds] = std::make_unique( Scales::noiseDecaySeconds.invmap(0.001), Scales::noiseDecaySeconds, "noiseDecaySeconds", Info::kCanAutomate); value[ID::halfClosedGain] = std::make_unique( - Scales::halfClosedGain.invmapDB(-40), Scales::halfClosedGain, "halfClosedGain", + Scales::halfClosedGain.invmapDB(-20), Scales::halfClosedGain, "halfClosedGain", Info::kCanAutomate); - value[ID::halfClosedDensity] = std::make_unique( - Scales::halfClosedDensity.invmap(1000), Scales::halfClosedDensity, - "halfClosedDensity", Info::kCanAutomate); - value[ID::halfClosedDecaySeconds] = std::make_unique( - Scales::noiseDecaySeconds.invmap(10.0), Scales::noiseDecaySeconds, - "halfClosedDecaySeconds", Info::kCanAutomate); + value[ID::halfCloseDecaySecond] = std::make_unique( + Scales::noiseDecaySeconds.invmap(1.0), Scales::noiseDecaySeconds, + "halfCloseDecaySecond", Info::kCanAutomate); + value[ID::halfCloseSustainLevel] = std::make_unique( + Scales::halfClosedGain.invmapDB(-20), Scales::halfClosedGain, + "halfCloseSustainLevel", Info::kCanAutomate); + value[ID::halfClosedPulseSecond] = std::make_unique( + Scales::noiseDecaySeconds.invmap(0.01), Scales::noiseDecaySeconds, + "halfClosedPulseSecond", Info::kCanAutomate); + value[ID::halfClosedDensityHz] = std::make_unique( + Scales::halfClosedDensityHz.invmap(500), Scales::halfClosedDensityHz, + "halfClosedDensityHz", Info::kCanAutomate); + value[ID::halfClosedHighpassHz] = std::make_unique( + Scales::halfClosedDensityHz.invmap(300), Scales::halfClosedDensityHz, + "halfClosedHighpassHz", Info::kCanAutomate); value[ID::closeGain] = std::make_unique( - Scales::gain.invmap(1.0), Scales::gain, "closeGain", Info::kCanAutomate); + Scales::halfClosedGain.invmap(0.1), Scales::halfClosedGain, "closeGain", + Info::kCanAutomate); value[ID::closeAttackSeconds] = std::make_unique( Scales::noiseDecaySeconds.invmap(0.05), Scales::noiseDecaySeconds, "closeAttackSeconds", Info::kCanAutomate); + value[ID::lossGain] = std::make_unique( - Scales::halfClosedGain.invmap(0.125), Scales::halfClosedGain, "lossGain", + Scales::halfClosedGain.invmap(1.0), Scales::halfClosedGain, "lossGain", Info::kCanAutomate); value[ID::delayTimeShape] = std::make_unique( @@ -219,7 +235,7 @@ struct GlobalParameter : public ParameterInterface { Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed1", Info::kCanAutomate); value[ID::allpassFeed2] = std::make_unique( - Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed2", + Scales::bipolarScale.invmap(-0.5), Scales::bipolarScale, "allpassFeed2", Info::kCanAutomate); value[ID::allpassMixSpike] = std::make_unique( Scales::defaultScale.invmap(2.0 / 3.0), Scales::defaultScale, "allpassMixSpike", @@ -229,10 +245,10 @@ struct GlobalParameter : public ParameterInterface { Info::kCanAutomate); value[ID::highShelfFrequencyHz] = std::make_unique( - Scales::cutoffFrequencyHz.invmap(8000.0), Scales::cutoffFrequencyHz, + Scales::cutoffFrequencyHz.invmap(12000.0), Scales::cutoffFrequencyHz, "highShelfFrequencyHz", Info::kCanAutomate); value[ID::highShelfGain] = std::make_unique( - Scales::shelvingGain.invmapDB(-1.0), Scales::shelvingGain, "highShelfGain", + Scales::shelvingGain.invmapDB(-0.5), Scales::shelvingGain, "highShelfGain", Info::kCanAutomate); value[ID::lowShelfFrequencyHz] = std::make_unique( Scales::cutoffFrequencyHz.invmap(20.0), Scales::cutoffFrequencyHz, diff --git a/common/dsp/scale.hpp b/common/dsp/scale.hpp index 3dec30aa..a7270f0b 100644 --- a/common/dsp/scale.hpp +++ b/common/dsp/scale.hpp @@ -20,8 +20,7 @@ #include #include #include - -#include "constants.hpp" +#include namespace SomeDSP { @@ -127,6 +126,9 @@ template class SPolyScale { // Based on superellipse. min < max. power > 0. template class EllipticScale { +private: + static constexpr T pi = std::numbers::pi_v; + public: EllipticScale(T min, T max, T power = T(2.0)) { set(min, max, power); } @@ -258,6 +260,9 @@ template class SemitoneScale { T getMin() { return minToZero ? 0 : minFreq; } T getMax() { return maxFreq; } + inline T noteToFreq(T note) { return T(440) * std::exp2((note - 69) / T(12)); } + inline T freqToNote(T freq) { return T(69) + T(12) * std::log2(freq / T(440)); } + protected: bool minToZero; T minNote; @@ -355,6 +360,50 @@ template class NegativeDecibelScale { T offset; }; +// Maps linear normalized value in [0, 1] to an exponential of exponential curve. It's too +// peaky and hard to use. The idea was something like: linear -> decibel -> decibel of +// decibel. +// +// Added to use for feedback or resonance. Increasing normalized value makes the raw value +// to be close to 1. +template class NegativeDoubleExpScale { +public: + NegativeDoubleExpScale(T minDB, bool maxToOne_) + { + const auto minAmp = std::pow(T(10), minDB / T(20)); + maxAmp = maxToOne_ ? T(0) : T(1) - minAmp; + minLog = std::log(minAmp); + maxToOne = maxToOne_; + } + + T map(T normalized) + { + if (maxToOne && normalized >= T(1)) return T(1); + return -std::expm1(-std::expm1(normalized * minLog) * minLog); + } + + T reverseMap(T input) const { return map(T(1) - input); } + + T invmap(T amplitude) + { + if (maxToOne && amplitude >= T(1)) return T(1); + return std::log1p(-std::log1p(-amplitude) / minLog) / minLog; + } + + T invmapDB(T dB) + { + return invmap(std::clamp(std::pow(T(10), dB / T(20)), getMin(), getMax())); + } + + T getMin() { return T(0); } + T getMax() { return maxToOne ? T(1) : maxAmp; } + +protected: + bool maxToOne; + T maxAmp; + T minLog; +}; + // DecibelScale, but can have negative values when normalized value is below `center`. // // - `center` is fixed to 0.5.