diff --git a/GenericDrum/source/dsp/dspcore.cpp b/GenericDrum/source/dsp/dspcore.cpp index d2e467a7..7c480fc8 100644 --- a/GenericDrum/source/dsp/dspcore.cpp +++ b/GenericDrum/source/dsp/dspcore.cpp @@ -152,6 +152,8 @@ void DSPCore::setup(double sampleRate) using ID = ParameterID::ID; \ const auto &pv = param.value; \ \ + preventBlowUp = pv[ID::preventBlowUp]->getInt(); \ + \ pitchSmoothingKp \ = EMAFilter::secondToP(upRate, pv[ID::noteSlideTimeSecond]->getDouble()); \ auto pitchBend \ @@ -170,7 +172,6 @@ void DSPCore::setup(double sampleRate) membraneWireMix.METHOD(pv[ID::membraneWireMix]->getDouble()); \ stereoBalance.METHOD(pv[ID::stereoBalance]->getDouble()); \ stereoMerge.METHOD(pv[ID::stereoMerge]->getDouble() / double(2)); \ - outputGain.METHOD(pv[ID::outputGain]->getDouble()); \ \ constexpr auto highpassQ = std::numbers::sqrt2_v / double(2); \ const auto highpassCut = pv[ID::safetyHighpassHz]->getDouble() / sampleRate; \ @@ -186,7 +187,7 @@ void DSPCore::setup(double sampleRate) } \ feedbackMatrix.constructHouseholder(); \ \ - const auto noiseLowpassHz = pv[ID::noiseLowpassHz]->getDouble() / upRate; \ + const auto noiseLowpassFreq = pv[ID::noiseLowpassHz]->getDouble() / upRate; \ const auto noiseAllpassMaxTimeHz = pv[ID::noiseAllpassMaxTimeHz]->getDouble(); \ const auto wireFrequencyHz = pv[ID::wireFrequencyHz]->getDouble(); \ const auto secondaryPitchOffset = pv[ID::secondaryPitchOffset]->getDouble(); \ @@ -198,8 +199,15 @@ void DSPCore::setup(double sampleRate) const auto bandpassCutSpread = pv[ID::bandpassCutSpread]->getDouble(); \ const auto pitchRandomCent = pv[ID::pitchRandomCent]->getDouble(); \ const size_t pitchType = pv[ID::pitchType]->getInt(); \ + \ + auto gain = pv[ID::outputGain]->getDouble(); \ + if (pv[ID::normalizeGainWrtNoiseLowpassHz]->getInt()) { \ + gain *= approxNormalizeGain(noiseLowpassFreq); \ + } \ + outputGain.METHOD(gain); \ + \ for (size_t drm = 0; drm < nDrum; ++drm) { \ - noiseLowpass[drm].METHOD(noiseLowpassHz); \ + noiseLowpass[drm].METHOD(noiseLowpassFreq); \ noiseAllpass[drm].timeInSamples.METHOD( \ prepareSerialAllpassTime(upRate, noiseAllpassMaxTimeHz, paramRng)); \ wireAllpass[drm].timeInSamples.METHOD( \ @@ -338,12 +346,6 @@ double DSPCore::processDrum( sig += noiseLowpass[index].process(noise); sig = std::tanh(noiseAllpass[index].process(sig, double(0.95))); - // Wire and membrane processing goes like: - // 1. solve collision, - // 2. update GUI status, - // 3. process delays, - // 4. update collision parameters. - // Wire. solveCollision( wirePosition[index], membrane1Position[index], wireVelocity[index], @@ -352,8 +354,9 @@ double DSPCore::processDrum( if (!isWireCollided && wirePosition[index] != 0) isWireCollided = true; auto wireCollision = std::lerp( - wireEnergyNoise[index].process(wirePosition[index], noiseRng), - wireEnergyDecay[index].process(wirePosition[index]), wireCollisionTypeMix.getValue()); + wireEnergyNoise[index].process(wirePosition[index], preventBlowUp, noiseRng), + wireEnergyDecay[index].process(wirePosition[index], preventBlowUp), + wireCollisionTypeMix.getValue()); wireCollision = double(8) * std::tanh(double(0.125) * wireCollision); const auto wireIn = double(0.995) * (sig + wireCollision); const auto wirePos = wireAllpass[index].process(wireIn, double(0.5)) * wireGain; @@ -375,14 +378,16 @@ double DSPCore::processDrum( feedbackMatrix.process(); const auto collision1 - = membrane1EnergyDecay[index].process(membrane1Position[index]) / double(maxFdnSize); + = membrane1EnergyDecay[index].process(membrane1Position[index], false) + / double(maxFdnSize); const auto p1 = membrane1[index].process( sig + collision1, crossGain, pitch, timeModAmt, feedbackMatrix); membrane1Velocity[index] = p1 - membrane1Position[index]; membrane1Position[index] = p1; const auto collision2 - = membrane2EnergyDecay[index].process(membrane2Position[index]) / double(maxFdnSize); + = membrane2EnergyDecay[index].process(membrane2Position[index], false) + / double(maxFdnSize); const auto p2 = membrane2[index].process( sig + collision2, crossGain, pitch, timeModAmt, feedbackMatrix); membrane2Velocity[index] = p2 - membrane2Position[index]; @@ -407,8 +412,8 @@ double DSPCore::processDrum( const auto merge = stereoMerge.process(); \ const auto outGain = outputGain.process(); \ \ - std::uniform_real_distribution dist{double(-1), double(1)}; \ - const auto noise = noiseGain * dist(noiseRng); \ + std::uniform_real_distribution dist{double(-0.5), double(0.5)}; \ + const auto noise = noiseGain * (dist(noiseRng) + dist(noiseRng)); \ noiseGain *= noiseDecay; \ wireGain *= wireDecay; diff --git a/GenericDrum/source/dsp/dspcore.hpp b/GenericDrum/source/dsp/dspcore.hpp index 5035425c..4175c487 100644 --- a/GenericDrum/source/dsp/dspcore.hpp +++ b/GenericDrum/source/dsp/dspcore.hpp @@ -143,6 +143,7 @@ class DSPCore { std::array, nDrum> noiseLowpass; std::array, nDrum> noiseAllpass; + bool preventBlowUp = false; std::array, 2> wireAllpass; std::array, 2> wireEnergyDecay; std::array, 2> wireEnergyNoise; diff --git a/GenericDrum/source/dsp/filter.hpp b/GenericDrum/source/dsp/filter.hpp index a22960c6..75719385 100644 --- a/GenericDrum/source/dsp/filter.hpp +++ b/GenericDrum/source/dsp/filter.hpp @@ -29,6 +29,20 @@ namespace SomeDSP { +// Normalize gain for `ComplexLowpass`. +// `x` is normalzied cutoff in [0, 0.5). +template inline Sample approxNormalizeGain(Sample x) +{ + constexpr std::array b{ + Sample(812352066809.4705), Sample(3.094123212331795e+16), + Sample(2.3874703361354637e+19), Sample(4.274664722153258e+20)}; + constexpr std::array a{ + Sample(-1650067658.2394962), Sample(1417724332731251.8), + Sample(2.7196907485733147e+19), Sample(1.1852026355744978e+22)}; + return x * (b[1] + x * (b[2] + x * (b[3] + x * b[4]))) + / (Sample(1) + x * (a[1] + x * (a[2] + x * (a[3] + x * a[4])))); +} + template class ComplexLowpass { private: Sample x1 = 0; @@ -190,7 +204,7 @@ template class Delay { void reset() { std::fill(buf.begin(), buf.end(), Sample(0)); } - Sample process(Sample input, Sample timeInSamples) + Sample processLinterp(Sample input, Sample timeInSamples) { const int size = int(buf.size()); @@ -208,6 +222,23 @@ template class Delay { if (rptr0 < 0) rptr0 += size; return std::lerp(buf[rptr0], buf[(rptr0 != 0 ? rptr0 : size) - 1], rFraction); } + + Sample process(Sample input, Sample timeInSamples) + { + const int size = int(buf.size()); + + // Set delay time. Min delay is set to 1 sample to avoid artifact of feedback. + const int timeInt = int(std::clamp(timeInSamples, Sample(1), Sample(size - 1))); + + // Write to buffer. + buf[wptr] = input; + if (++wptr >= size) wptr = 0; + + // Read from buffer. + int rptr0 = wptr - timeInt; + if (rptr0 < 0) rptr0 += size; + return buf[rptr0]; + } }; template class ParallelRateLimiter { @@ -294,10 +325,11 @@ template class EnergyStoreDecay { void reset() { sum = 0; } - Sample process(Sample value) + Sample process(Sample value, bool preventBlowUp) { const auto absed = std::abs(value); if (absed > eps) sum = (sum + value) * decay; + if (preventBlowUp) sum = std::min(Sample(1) / Sample(4), sum); return sum *= gain; } }; @@ -309,10 +341,11 @@ template class EnergyStoreNoise { public: void reset() { sum = 0; } - Sample process(Sample value, Rng &rng) + Sample process(Sample value, bool preventBlowUp, Rng &rng) { sum += std::abs(value); - std::uniform_real_distribution dist{Sample(-sum), Sample(sum)}; + const auto range = preventBlowUp ? std::min(Sample(1) / Sample(64), sum) : sum; + std::uniform_real_distribution dist{Sample(-range), Sample(range)}; const auto out = dist(rng); sum -= std::abs(out); return out; @@ -344,7 +377,7 @@ template class ParallelDelay { for (auto &bf : buffer) std::fill(bf.begin(), bf.end(), Sample(0)); } - void process( + void processLinterp( std::array &input, const std::array &timeInSamples, Sample timeScaler) @@ -369,6 +402,30 @@ template class ParallelDelay { input[idx] = std::lerp(bf[rptr0], bf[(rptr0 != 0 ? rptr0 : size) - 1], rFraction); } } + + void process( + std::array &input, + const std::array &timeInSamples, + Sample timeScaler) + { + for (size_t idx = 0; idx < length; ++idx) { + auto &bf = buffer[idx]; + const int size = int(bf.size()); + + // Set delay time. Min delay is set to 1 sample to avoid artifact of feedback. + int timeInt + = int(std::clamp(timeInSamples[idx] / timeScaler, Sample(1), Sample(size - 1))); + + // Write to buffer. + bf[wptr[idx]] = input[idx]; + if (++wptr[idx] >= size) wptr[idx] = 0; + + // Read from buffer. + int rptr0 = wptr[idx] - timeInt; + if (rptr0 < 0) rptr0 += size; + input[idx] = bf[rptr0]; + } + } }; template class MembraneDelayRateLimiter { diff --git a/GenericDrum/source/editor.cpp b/GenericDrum/source/editor.cpp index 0068685f..c3482a62 100644 --- a/GenericDrum/source/editor.cpp +++ b/GenericDrum/source/editor.cpp @@ -89,20 +89,6 @@ void Editor::valueChanged(CControl *pControl) ParamID id = pControl->getTag(); if (id != ID::isWireCollided && id != ID::isSecondaryCollided) { - // controller->setParamNormalized(ID::isWireCollided, 0.0); - // controller->performEdit(ID::isWireCollided, 0.0); - // if (labelWireCollision.get()) { - // labelWireCollision->setText(wireDidntCollidedText); - // labelWireCollision->setDirty(); - // } - - // controller->setParamNormalized(ID::isSecondaryCollided, 0.0); - // controller->performEdit(ID::isSecondaryCollided, 0.0); - // if (labelMembraneCollision.get()) { - // labelMembraneCollision->setText(membraneDidntCollidedText); - // labelMembraneCollision->setDirty(); - // } - resetStatusText( controller, labelWireCollision, ID::isWireCollided, wireDidntCollidedText); resetStatusText( @@ -180,6 +166,7 @@ bool Editor::prepareUI() constexpr auto mixTop4 = mixTop0 + 4 * labelY; constexpr auto mixTop5 = mixTop0 + 5 * labelY; constexpr auto mixTop6 = mixTop0 + 6 * labelY; + constexpr auto mixTop7 = mixTop0 + 7 * labelY; constexpr auto mixLeft0 = left0; constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin; addGroupLabel(mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix"); @@ -195,26 +182,31 @@ bool Editor::prepareUI() mixLeft1, mixTop2, labelWidth, labelHeight, uiTextSize, ID::safetyHighpassHz, Scales::safetyHighpassHz, false, 5); addCheckbox( - mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "Reset Seed at Note-on", + mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "2x Sampling", + ID::overSampling); + addCheckbox( + mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, "Normalize Gain", + ID::normalizeGainWrtNoiseLowpassHz); + addCheckbox( + mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Reset Seed at Note-on", ID::resetSeedAtNoteOn); addCheckbox( - mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, "2x Sampling", - ID::overSampling); - addLabel(mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Channel"); - addOptionMenu( - mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, ID::stereoEnable, - {"Mono", "Stereo"}); - addLabel(mixLeft0, mixTop5, labelWidth, labelHeight, uiTextSize, "Stereo Balance"); + mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, "Prevent Blow Up", + ID::preventBlowUp); + addToggleButton( + mixLeft0, mixTop5, groupLabelWidth, labelHeight, uiTextSize, "Stereo Unison", + ID::stereoEnable); + addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Stereo Balance"); addTextKnob( - mixLeft1, mixTop5, labelWidth, labelHeight, uiTextSize, ID::stereoBalance, + mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::stereoBalance, Scales::bipolarScale, false, 5); - addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Stereo Merge"); + addLabel(mixLeft0, mixTop7, labelWidth, labelHeight, uiTextSize, "Stereo Merge"); addTextKnob( - mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::stereoMerge, + mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::stereoMerge, Scales::defaultScale, false, 5); // Tuning. - constexpr auto tuningTop0 = top0 + 7 * labelY; + constexpr auto tuningTop0 = top0 + 8 * labelY; constexpr auto tuningTop1 = tuningTop0 + 1 * labelY; constexpr auto tuningTop2 = tuningTop0 + 2 * labelY; constexpr auto tuningTop3 = tuningTop0 + 3 * labelY; @@ -322,7 +314,7 @@ bool Editor::prepareUI() wireLeft1, wireTop6, labelWidth, labelHeight, uiTextSize, ID::wireCollisionTypeMix, Scales::defaultScale, false, 5); labelWireCollision = addLabel( - wireLeft0, wireTop7, 2 * labelWidth, labelHeight, uiTextSize, + wireLeft0, wireTop7, groupLabelWidth, labelHeight, uiTextSize, "Wire collision status."); // Primary Membrane. @@ -466,12 +458,12 @@ bool Editor::prepareUI() secondaryLeft1, secondaryTop4, labelWidth, labelHeight, uiTextSize, ID::secondaryDistance, Scales::collisionDistance, false, 5); labelMembraneCollision = addLabel( - secondaryLeft0, secondaryTop5, 2 * labelWidth, labelHeight, uiTextSize, + secondaryLeft0, secondaryTop5, groupLabelWidth, labelHeight, uiTextSize, "Membrane collision status."); // Plugin name. constexpr auto splashMargin = uiMargin; - constexpr auto splashTop = top0 + 13 * labelY + int(labelHeight / 4) + 2 * margin; + constexpr auto splashTop = top0 + 18 * labelY + int(labelHeight / 4) + 2 * margin; constexpr auto splashLeft = left0 + int(labelWidth / 4); addSplashScreen( splashLeft, splashTop, splashWidth, splashHeight, splashMargin, splashMargin, diff --git a/GenericDrum/source/parameter.cpp b/GenericDrum/source/parameter.cpp index ea653663..64a4719c 100644 --- a/GenericDrum/source/parameter.cpp +++ b/GenericDrum/source/parameter.cpp @@ -40,7 +40,7 @@ DecibelScale Scales::safetyHighpassHz(ampToDB(0.1), ampToDB(100.0), fals UIntScale Scales::semitone(semitoneOffset + 48); LinearScale Scales::cent(-100.0, 100.0); LinearScale Scales::pitchBendRange(0.0, 120.0); -DecibelScale Scales::noteSlideTimeSecond(-100.0, 40.0, true); +DecibelScale Scales::noteSlideTimeSecond(-40.0, 40.0, false); DecibelScale Scales::noiseDecaySeconds(-40, ampToDB(0.5), false); diff --git a/GenericDrum/source/parameter.hpp b/GenericDrum/source/parameter.hpp index 20d8847f..a0e1b40c 100644 --- a/GenericDrum/source/parameter.hpp +++ b/GenericDrum/source/parameter.hpp @@ -44,8 +44,10 @@ enum ID { outputGain, safetyHighpassEnable, safetyHighpassHz, - resetSeedAtNoteOn, overSampling, + normalizeGainWrtNoiseLowpassHz, + resetSeedAtNoteOn, + preventBlowUp, stereoEnable, stereoBalance, @@ -158,10 +160,14 @@ struct GlobalParameter : public ParameterInterface { value[ID::safetyHighpassHz] = std::make_unique( Scales::safetyHighpassHz.invmap(15.0), Scales::safetyHighpassHz, "safetyHighpassHz", Info::kCanAutomate); - value[ID::resetSeedAtNoteOn] = std::make_unique( - 1, Scales::boolScale, "resetSeedAtNoteOn", Info::kCanAutomate); value[ID::overSampling] = std::make_unique( 1, Scales::boolScale, "overSampling", Info::kCanAutomate); + value[ID::normalizeGainWrtNoiseLowpassHz] = std::make_unique( + 1, Scales::boolScale, "normalizeGainWrtNoiseLowpassHz", Info::kCanAutomate); + value[ID::resetSeedAtNoteOn] = std::make_unique( + 1, Scales::boolScale, "resetSeedAtNoteOn", Info::kCanAutomate); + value[ID::preventBlowUp] = std::make_unique( + 0, Scales::boolScale, "preventBlowUp", Info::kCanAutomate); value[ID::stereoEnable] = std::make_unique( 1, Scales::boolScale, "stereoEnable", Info::kCanAutomate); @@ -185,7 +191,7 @@ struct GlobalParameter : public ParameterInterface { Scales::pitchBendRange.invmap(2.0), Scales::pitchBendRange, "pitchBendRange", Info::kCanAutomate); value[ID::noteSlideTimeSecond] = std::make_unique( - Scales::noteSlideTimeSecond.invmap(0.01), Scales::noteSlideTimeSecond, + Scales::noteSlideTimeSecond.invmap(0.1), Scales::noteSlideTimeSecond, "noteSlideTimeSecond", Info::kCanAutomate); value[ID::seed]