From e41accebe383eb4cbb47f1bb7f2a767dd1f18ef6 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Wed, 4 Oct 2023 19:08:18 +0900 Subject: [PATCH] Add external input to GenericDrum --- GenericDrum/source/dsp/dspcore.cpp | 113 ++++++++++++++++++++------- GenericDrum/source/dsp/dspcore.hpp | 16 +++- GenericDrum/source/dsp/filter.hpp | 30 ++++++- GenericDrum/source/editor.cpp | 62 ++++++++++++++- GenericDrum/source/editor.hpp | 3 + GenericDrum/source/parameter.cpp | 2 + GenericDrum/source/parameter.hpp | 23 +++++- GenericDrum/source/plugfactory.cpp | 2 +- GenericDrum/source/plugprocessor.cpp | 7 +- GenericDrum/source/version.hpp | 8 +- 10 files changed, 218 insertions(+), 48 deletions(-) diff --git a/GenericDrum/source/dsp/dspcore.cpp b/GenericDrum/source/dsp/dspcore.cpp index 5460278d..c1594192 100644 --- a/GenericDrum/source/dsp/dspcore.cpp +++ b/GenericDrum/source/dsp/dspcore.cpp @@ -135,6 +135,8 @@ void DSPCore::setup(double sampleRate) SmootherCommon::setTime(double(0.2)); + triggerDetector.setup(upRate * double(0.125)); + const auto maxDelayTimeSamples = upRate; for (auto &x : noiseAllpass) x.setup(maxDelayTimeSamples); for (auto &x : wireAllpass) x.setup(maxDelayTimeSamples); @@ -153,6 +155,8 @@ void DSPCore::setup(double sampleRate) using ID = ParameterID::ID; \ const auto &pv = param.value; \ \ + useExternalInput = pv[ID::useExternalInput]->getInt(); \ + useAutomaticTrigger = pv[ID::useAutomaticTrigger]->getInt() && useExternalInput; \ preventBlowUp = pv[ID::preventBlowUp]->getInt(); \ \ pitchSmoothingKp \ @@ -162,6 +166,7 @@ void DSPCore::setup(double sampleRate) auto notePitch = calcNotePitch(pitchBend * noteNumber); \ interpPitch.METHOD(notePitch); \ \ + externalInputGain.METHOD(pv[ID::externalInputGain]->getDouble()); \ wireDistance.METHOD(pv[ID::wireDistance]->getDouble()); \ wireCollisionTypeMix.METHOD(pv[ID::wireCollisionTypeMix]->getDouble()); \ impactWireMix.METHOD(pv[ID::impactWireMix]->getDouble()); \ @@ -180,15 +185,25 @@ void DSPCore::setup(double sampleRate) safetyHighpass[ch].METHOD(highpassCut, highpassQ); \ } \ \ + triggerDetector.prepare(pv[ID::automaticTriggerThreshold]->getDouble()); \ + \ paramRng.seed(pv[ID::seed]->getInt()); \ \ + const auto noiseLowpassFreq = pv[ID::noiseLowpassHz]->getDouble() / upRate; \ + noiseLowpass.METHOD(noiseLowpassFreq); \ + \ + auto gain = pv[ID::outputGain]->getDouble(); \ + if (pv[ID::normalizeGainWrtNoiseLowpassHz]->getInt()) { \ + gain *= approxNormalizeGain(noiseLowpassFreq) / interpPitch.getValue(); \ + } \ + outputGain.METHOD(gain); \ + \ for (size_t idx = 0; idx < maxFdnSize; ++idx) { \ const auto crossFeedbackRatio = pv[ID::crossFeedbackRatio0 + idx]->getDouble(); \ feedbackMatrix.seed[idx] = crossFeedbackRatio * crossFeedbackRatio; \ } \ feedbackMatrix.constructHouseholder(); \ \ - 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(); \ @@ -201,14 +216,7 @@ void DSPCore::setup(double sampleRate) 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) / interpPitch.getValue(); \ - } \ - outputGain.METHOD(gain); \ - \ for (size_t drm = 0; drm < nDrum; ++drm) { \ - noiseLowpass[drm].METHOD(noiseLowpassFreq); \ noiseAllpass[drm].timeInSamples.METHOD( \ prepareSerialAllpassTime(upRate, noiseAllpassMaxTimeHz, paramRng)); \ wireAllpass[drm].timeInSamples.METHOD( \ @@ -274,6 +282,8 @@ void DSPCore::reset() noteNumber = 69.0; velocity = 0; + triggerDetector.reset(); + noiseGain = 0; noiseDecay = 0; for (auto &x : noiseAllpass) x.reset(); @@ -289,6 +299,7 @@ void DSPCore::reset() envelope.reset(); releaseSmoother.reset(); + feedbackMatrix.reset(); membrane1Position.fill({}); membrane1Velocity.fill({}); membrane2Position.fill({}); @@ -298,6 +309,7 @@ void DSPCore::reset() for (auto &x : membrane1) x.reset(); for (auto &x : membrane2) x.reset(); + for (auto &x : halfbandInput) x.fill({}); for (auto &x : halfbandIir) x.reset(); } @@ -340,17 +352,14 @@ inline void solveCollision(double &p0, double &p1, double v0, double v1, double double DSPCore::processDrum( size_t index, - double noise, + double excitation, double wireGain, double pitchEnv, double crossGain, double timeModAmt) { - // Impact. - constexpr auto eps = std::numeric_limits::epsilon(); - double sig = 0; - sig += noiseLowpass[index].process(noise); - sig = std::tanh(noiseAllpass[index].process(sig, double(0.95))); + // Impact & Echo. + double sig = std::tanh(noiseAllpass[index].process(excitation, double(0.95))); // Wire. solveCollision( @@ -365,7 +374,8 @@ double DSPCore::processDrum( 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; + auto wirePos = wireAllpass[index].process(wireIn, double(0.5)) * wireGain; + if (preventBlowUp) wirePos /= double(nAllpass); wireVelocity[index] = wirePos - wirePosition[index]; wirePosition[index] = wirePos; @@ -405,6 +415,7 @@ double DSPCore::processDrum( } #define PROCESS_COMMON \ + externalInputGain.process(); \ wireDistance.process(); \ wireCollisionTypeMix.process(); \ impactWireMix.process(); \ @@ -418,25 +429,45 @@ double DSPCore::processDrum( const auto outGain = outputGain.process(); \ \ std::uniform_real_distribution dist{double(-0.5), double(0.5)}; \ - const auto noise = noiseGain * (dist(noiseRng) + dist(noiseRng)); \ + const auto noise \ + = noiseLowpass.process(noiseGain * (dist(noiseRng) + dist(noiseRng))); \ noiseGain *= noiseDecay; \ wireGain *= wireDecay; \ \ const auto pitchEnv = std::exp2(envelope.process() + releaseSmoother.process()); -double DSPCore::processSample() +inline void DSPCore::processExternalInput(double absed) +{ + if (maxExtInAmplitude < absed) maxExtInAmplitude = absed; + if (useAutomaticTrigger && triggerDetector.process(absed)) wireGain = double(2); +} + +double DSPCore::processSample(double externalInput) { PROCESS_COMMON; - return outGain * processDrum(0, noise, wireGain, pitchEnv, crossGain, timeModAmt); + const auto excitation + = useExternalInput ? externalInput * externalInputGain.getValue() : noise; + if (useExternalInput) { + processExternalInput(std::abs(excitation)); + } + + return outGain * processDrum(0, excitation, wireGain, pitchEnv, crossGain, timeModAmt); } -std::array DSPCore::processFrame() +std::array DSPCore::processFrame(const std::array &externalInput) { PROCESS_COMMON; - auto drum0 = processDrum(0, noise, wireGain, pitchEnv, crossGain, timeModAmt); - auto drum1 = processDrum(1, noise, wireGain, pitchEnv, crossGain, timeModAmt); + const auto &extGain = externalInputGain.getValue(); + const auto excitation0 = useExternalInput ? externalInput[0] * extGain : noise; + const auto excitation1 = useExternalInput ? externalInput[1] * extGain : noise; + if (useExternalInput) { + processExternalInput(double(0.5) * (std::abs(excitation0) + std::abs(excitation1))); + } + + auto drum0 = processDrum(0, excitation0, wireGain, pitchEnv, crossGain, timeModAmt); + auto drum1 = processDrum(1, excitation1, wireGain, pitchEnv, crossGain, timeModAmt); constexpr auto eps = std::numeric_limits::epsilon(); if (balance < -eps) { @@ -450,7 +481,8 @@ std::array DSPCore::processFrame() }; } -void DSPCore::process(const size_t length, float *out0, float *out1) +void DSPCore::process( + const size_t length, const float *in0, const float *in1, float *out0, float *out1) { ScopedNoDenormals scopedDenormals; @@ -463,17 +495,29 @@ void DSPCore::process(const size_t length, float *out0, float *out1) bool isStereo = pv[ID::stereoUnison]->getInt(); bool isSafetyHighpassEnabled = pv[ID::safetyHighpassEnable]->getInt(); + maxExtInAmplitude = 0; + + std::array prevExtIn = halfbandInput[0]; std::array frame{}; for (size_t i = 0; i < length; ++i) { processMidiNote(i); + const double extIn0 = in0 == nullptr ? 0 : in0[i]; + const double extIn1 = in1 == nullptr ? 0 : in1[i]; + if (isStereo) { if (overSampling) { - for (size_t j = 0; j < upFold; ++j) { - frame = processFrame(); - halfbandInput[0][j] = frame[0]; - halfbandInput[1][j] = frame[1]; - } + frame = processFrame({ + double(0.5) * (prevExtIn[0] + extIn0), + double(0.5) * (prevExtIn[1] + extIn1), + }); + halfbandInput[0][0] = frame[0]; + halfbandInput[1][0] = frame[1]; + + frame = processFrame({extIn0, extIn1}); + halfbandInput[0][1] = frame[0]; + halfbandInput[1][1] = frame[1]; + frame[0] = halfbandIir[0].process(halfbandInput[0]); frame[1] = halfbandIir[1].process(halfbandInput[1]); if (isSafetyHighpassEnabled) { @@ -483,7 +527,7 @@ void DSPCore::process(const size_t length, float *out0, float *out1) out0[i] = float(frame[0]); out1[i] = float(frame[1]); } else { - frame = processFrame(); + frame = processFrame({extIn0, extIn1}); if (isSafetyHighpassEnabled) { frame[0] = safetyHighpass[0].process(frame[0]); frame[1] = safetyHighpass[1].process(frame[1]); @@ -492,22 +536,31 @@ void DSPCore::process(const size_t length, float *out0, float *out1) out1[i] = float(frame[1]); } } else { + const double extInMixed = double(0.5) * (extIn0 + extIn1); if (overSampling) { - for (size_t j = 0; j < upFold; ++j) halfbandInput[0][j] = processSample(); + halfbandInput[0][0] + = processSample(extInMixed + double(0.5) * (prevExtIn[0] + prevExtIn[1])); + halfbandInput[0][1] = processSample(extInMixed); frame[0] = halfbandIir[0].process(halfbandInput[0]); if (isSafetyHighpassEnabled) frame[0] = safetyHighpass[0].process(frame[0]); out0[i] = float(frame[0]); out1[i] = float(frame[0]); } else { - frame[0] = processSample(); + frame[0] = processSample(extInMixed); if (isSafetyHighpassEnabled) frame[0] = safetyHighpass[0].process(frame[0]); out0[i] = float(frame[0]); out1[i] = float(frame[0]); } } + + prevExtIn = {extIn0, extIn1}; } + // Propagate last input to next cycle. + halfbandInput[0] = prevExtIn; + // Send a value to GUI. + pv[ID::externalInputAmplitudeMeter]->setFromFloat(maxExtInAmplitude); if (isWireCollided) pv[ID::isWireCollided]->setFromInt(1); if (isSecondaryCollided) pv[ID::isSecondaryCollided]->setFromInt(1); } diff --git a/GenericDrum/source/dsp/dspcore.hpp b/GenericDrum/source/dsp/dspcore.hpp index f63db13b..6429e335 100644 --- a/GenericDrum/source/dsp/dspcore.hpp +++ b/GenericDrum/source/dsp/dspcore.hpp @@ -56,7 +56,8 @@ class DSPCore { void reset(); void startup(); void setParameters(); - void process(const size_t length, float *out0, float *out1); + void process( + const size_t length, const float *in0, const float *in1, float *out0, float *out1); void noteOn(NoteInfo &info); void noteOff(int_fast32_t noteId); @@ -96,8 +97,8 @@ class DSPCore { void updateUpRate(); void resetCollision(); double calcNotePitch(double note); - double processSample(); - std::array processFrame(); + double processSample(double externalInput); + std::array processFrame(const std::array &externalInput); double processDrum( size_t index, double noise, @@ -105,10 +106,12 @@ class DSPCore { double pitchEnv, double crossGain, double timeModAmt); + inline void processExternalInput(double absed); std::vector midiNotes; std::vector noteStack; + double maxExtInAmplitude = 0; bool isWireCollided = false; bool isSecondaryCollided = false; @@ -126,6 +129,7 @@ class DSPCore { double pitchSmoothingKp = 1.0; ExpSmootherLocal interpPitch; + ExpSmoother externalInputGain; ExpSmoother wireDistance; ExpSmoother wireCollisionTypeMix; ExpSmoother impactWireMix; @@ -141,11 +145,15 @@ class DSPCore { static constexpr size_t nDrum = 2; static constexpr size_t nAllpass = 4; + bool useExternalInput = false; + bool useAutomaticTrigger = false; + TriggerDetector triggerDetector; + std::minstd_rand noiseRng{0}; std::minstd_rand paramRng{0}; double noiseGain = 0; double noiseDecay = 0; - std::array, nDrum> noiseLowpass; + ComplexLowpass noiseLowpass; std::array, nDrum> noiseAllpass; bool preventBlowUp = false; diff --git a/GenericDrum/source/dsp/filter.hpp b/GenericDrum/source/dsp/filter.hpp index 75719385..4f14a775 100644 --- a/GenericDrum/source/dsp/filter.hpp +++ b/GenericDrum/source/dsp/filter.hpp @@ -20,6 +20,7 @@ #include "../../../common/dsp/smoother.hpp" #include #include +#include #include #include #include @@ -29,6 +30,29 @@ namespace SomeDSP { +template class TriggerDetector { +private: + Sample v0 = 0; + Sample decay = 0; + Sample threshold = 0; + +public: + void setup(Sample decayTimeSamples) + { + decay = std::pow(Sample(1e-3), Sample(1) / decayTimeSamples); + } + + void reset() { v0 = 0; } + void prepare(Sample newThreshold) { threshold = newThreshold; } + + bool process(Sample absed) + { + const auto v1 = v0; + v0 = v0 < absed ? absed : v0 * decay; + return v0 >= threshold && v1 < threshold; + } +}; + // Normalize gain for `ComplexLowpass`. // `x` is normalzied cutoff in [0, 0.5). template inline Sample approxNormalizeGain(Sample x) @@ -329,7 +353,7 @@ template class EnergyStoreDecay { { const auto absed = std::abs(value); if (absed > eps) sum = (sum + value) * decay; - if (preventBlowUp) sum = std::min(Sample(1) / Sample(4), sum); + if (preventBlowUp) sum = std::min(Sample(1) / Sample(8), sum); return sum *= gain; } }; @@ -344,8 +368,8 @@ template class EnergyStoreNoise { Sample process(Sample value, bool preventBlowUp, Rng &rng) { sum += std::abs(value); - const auto range = preventBlowUp ? std::min(Sample(1) / Sample(64), sum) : sum; - std::uniform_real_distribution dist{Sample(-range), Sample(range)}; + if (preventBlowUp) sum = std::min(Sample(1) / Sample(4), sum); + std::uniform_real_distribution dist{Sample(-sum), Sample(sum)}; const auto out = dist(rng); sum -= std::abs(out); return out; diff --git a/GenericDrum/source/editor.cpp b/GenericDrum/source/editor.cpp index 5affc467..391251bc 100644 --- a/GenericDrum/source/editor.cpp +++ b/GenericDrum/source/editor.cpp @@ -20,7 +20,9 @@ #include "version.hpp" #include -#include +#include +#include +#include constexpr float uiTextSize = 12.0f; constexpr float pluginNameTextSize = 14.0f; @@ -107,7 +109,35 @@ void Editor::updateUI(ParamID id, ParamValue normalized) PlugEditor::updateUI(id, normalized); - if (labelWireCollision.get() && id == ID::isWireCollided) { + if (labelExternalInputAmplitude.get() && id == ID::externalInputAmplitudeMeter) { + if (getPlainValue(ID::useExternalInput) == 0) { + labelExternalInputAmplitude->setText("External input is disabled."); + labelExternalInputAmplitude->setDirty(); + extInPeakDecibel = -std::numeric_limits::infinity(); + } else { + const auto decibel + = 20 * std::log10(getPlainValue(ID::externalInputAmplitudeMeter)); + if (extInPeakDecibel < decibel) { + extInPeakDecibel = decibel; + extInPeakHoldCounter = 60; // Bad, because display refresh rate isn't considered. + } + + std::ostringstream os; + os.precision(5); + os << std::fixed << "Ext. Peak: " << extInPeakDecibel << " [dB]"; + labelExternalInputAmplitude->setText(os.str()); + labelExternalInputAmplitude->setDirty(); + + if (extInPeakHoldCounter == 0) { + extInPeakDecibel -= double(0.1); + if (extInPeakDecibel < -60) { + extInPeakDecibel = -std::numeric_limits::infinity(); + } + } else { + --extInPeakHoldCounter; + } + } + } else if (labelWireCollision.get() && id == ID::isWireCollided) { if (getPlainValue(ID::isWireCollided)) { labelWireCollision->setText("Wire collided."); } else { @@ -167,9 +197,14 @@ bool Editor::prepareUI() constexpr auto mixTop5 = mixTop0 + 5 * labelY; constexpr auto mixTop6 = mixTop0 + 6 * labelY; constexpr auto mixTop7 = mixTop0 + 7 * labelY; + constexpr auto mixTop8 = mixTop0 + 8 * labelY; + constexpr auto mixTop9 = mixTop0 + 9 * labelY; + constexpr auto mixTop10 = mixTop0 + 10 * labelY; + constexpr auto mixTop11 = mixTop0 + 11 * labelY; constexpr auto mixLeft0 = left0; constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin; - addGroupLabel(mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix"); + addGroupLabel( + mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix & Options"); addLabel(mixLeft0, mixTop1, labelWidth, labelHeight, uiTextSize, "Output [dB]"); addTextKnob( @@ -193,6 +228,7 @@ bool Editor::prepareUI() addCheckbox( mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, "Prevent Blow Up", ID::preventBlowUp); + addToggleButton( mixLeft0, mixTop5, groupLabelWidth, labelHeight, uiTextSize, "Stereo Unison", ID::stereoUnison); @@ -205,8 +241,26 @@ bool Editor::prepareUI() mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::stereoMerge, Scales::defaultScale, false, 5); + addToggleButton( + mixLeft0, mixTop8, groupLabelWidth, labelHeight, uiTextSize, "External Input", + ID::useExternalInput); + addLabel(mixLeft0, mixTop9, labelWidth, labelHeight, uiTextSize, "External Gain [dB]"); + addTextKnob( + mixLeft1, mixTop9, labelWidth, labelHeight, uiTextSize, ID::externalInputGain, + Scales::gain, true, 5); + addLabel( + mixLeft0, mixTop10, labelWidth, labelHeight, uiTextSize, "Trigger Threshold [dB]"); + addTextKnob( + mixLeft1, mixTop10, labelWidth, labelHeight, uiTextSize, + ID::automaticTriggerThreshold, Scales::gain, true, 5); + addCheckbox( + mixLeft0, mixTop11, labelWidth, labelHeight, uiTextSize, "Automatic Trigger", + ID::useAutomaticTrigger); + labelExternalInputAmplitude + = addLabel(mixLeft1, mixTop11, labelWidth, labelHeight, uiTextSize, "Initialized."); + // Tuning. - constexpr auto tuningTop0 = top0 + 8 * labelY; + constexpr auto tuningTop0 = top0 + 12 * labelY; constexpr auto tuningTop1 = tuningTop0 + 1 * labelY; constexpr auto tuningTop2 = tuningTop0 + 2 * labelY; constexpr auto tuningTop3 = tuningTop0 + 3 * labelY; diff --git a/GenericDrum/source/editor.hpp b/GenericDrum/source/editor.hpp index 4b54784e..d220e136 100644 --- a/GenericDrum/source/editor.hpp +++ b/GenericDrum/source/editor.hpp @@ -42,6 +42,9 @@ class Editor : public PlugEditor { ParamValue getPlainValue(ParamID id); bool prepareUI() override; + size_t extInPeakHoldCounter = 0; + double extInPeakDecibel = 0; + SharedPointer