diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0bf48978..52523293 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -58,4 +58,5 @@ smtg_enable_vst3_sdk()
# ## Below are prototype plugins. Breaking changes will be introduced.
# add_subdirectory(TestBedSynth)
-add_subdirectory(GrowlSynth)
+# add_subdirectory(GrowlSynth)
+add_subdirectory(GenericDrum)
diff --git a/GenericDrum/CMakeLists.txt b/GenericDrum/CMakeLists.txt
new file mode 100644
index 00000000..b8036d30
--- /dev/null
+++ b/GenericDrum/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.20)
+
+
+include(../common/cmake/non_simd.cmake)
+
+if(TEST_PLUGIN)
+ build_test("")
+else()
+ # VST 3 source files.
+ set(plug_sources
+ source/parameter.cpp
+ source/gui/splashdraw.cpp
+ source/plugprocessor.cpp
+ source/editor.cpp
+ source/plugfactory.cpp)
+
+ build_vst3("${plug_sources}")
+endif()
diff --git a/GenericDrum/resource/Info.plist b/GenericDrum/resource/Info.plist
new file mode 100644
index 00000000..ae89dcc4
--- /dev/null
+++ b/GenericDrum/resource/Info.plist
@@ -0,0 +1,28 @@
+
+
+
+
+ NSHumanReadableCopyright
+ 2018 Steinberg Media Technologies
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ GenericDrum
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ com.steinberg.vst3.GenericDrum
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ CFBundleShortVersionString
+ 1.0
+ CSResourcesFileMapped
+
+
+
diff --git a/GenericDrum/resource/plug.rc b/GenericDrum/resource/plug.rc
new file mode 100644
index 00000000..128ff068
--- /dev/null
+++ b/GenericDrum/resource/plug.rc
@@ -0,0 +1,44 @@
+#include
+#include "../source/version.hpp"
+
+#define APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// Version
+/////////////////////////////////////////////////////////////////////////////
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ PRODUCTVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040004e4"
+ BEGIN
+ VALUE "FileVersion", FULL_VERSION_STR"\0"
+ VALUE "ProductVersion", FULL_VERSION_STR"\0"
+ VALUE "OriginalFilename", stringOriginalFilename"\0"
+ VALUE "FileDescription", stringFileDescription"\0"
+ VALUE "InternalName", stringFileDescription"\0"
+ VALUE "ProductName", stringFileDescription"\0"
+ VALUE "CompanyName", stringCompanyName"\0"
+ VALUE "LegalCopyright", stringLegalCopyright"\0"
+ VALUE "LegalTrademarks", stringLegalTrademarks"\0"
+ //VALUE "PrivateBuild", " \0"
+ //VALUE "SpecialBuild", " \0"
+ //VALUE "Comments", " \0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x400, 1252
+ END
+END
diff --git a/GenericDrum/source/controller.hpp b/GenericDrum/source/controller.hpp
new file mode 100644
index 00000000..4dc8b5a8
--- /dev/null
+++ b/GenericDrum/source/controller.hpp
@@ -0,0 +1,85 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "../../common/plugcontroller.hpp"
+#include "parameter.hpp"
+
+namespace Steinberg {
+namespace Synth {
+
+template
+tresult PLUGIN_API PlugController::getMidiControllerAssignment(
+ int32 busIndex, int16 channel, Vst::CtrlNumber midiControllerNumber, Vst::ParamID &id)
+{
+ switch (midiControllerNumber) {
+ // case Vst::kCtrlExpression:
+ // case Vst::kCtrlVolume:
+ // id = ParameterID::gain;
+ // return kResultOk;
+
+ case Vst::kPitchBend:
+ id = ParameterID::pitchBend;
+ return kResultOk;
+ }
+ return kResultFalse;
+}
+
+template
+int32 PLUGIN_API PlugController::getNoteExpressionCount(
+ int32 busIndex, int16 channel)
+{
+ return 0;
+}
+
+template
+tresult PLUGIN_API PlugController::getNoteExpressionInfo(
+ int32 busIndex,
+ int16 channel,
+ int32 noteExpressionIndex,
+ Vst::NoteExpressionTypeInfo &info)
+{
+ return kResultFalse;
+}
+
+template
+tresult PLUGIN_API
+PlugController::getNoteExpressionStringByValue(
+ int32 busIndex,
+ int16 channel,
+ Vst::NoteExpressionTypeID id,
+ Vst::NoteExpressionValue valueNormalized,
+ Vst::String128 string)
+{
+ return kResultFalse;
+}
+
+template
+tresult PLUGIN_API
+PlugController::getNoteExpressionValueByString(
+ int32 busIndex,
+ int16 channel,
+ Vst::NoteExpressionTypeID id,
+ const Vst::TChar *string,
+ Vst::NoteExpressionValue &valueNormalized)
+{
+ return kResultFalse;
+}
+
+} // namespace Synth
+} // namespace Steinberg
diff --git a/GenericDrum/source/dsp/dspcore.cpp b/GenericDrum/source/dsp/dspcore.cpp
new file mode 100644
index 00000000..0fc0847d
--- /dev/null
+++ b/GenericDrum/source/dsp/dspcore.cpp
@@ -0,0 +1,469 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#include "../../../lib/juce_ScopedNoDenormal.hpp"
+
+#include "dspcore.hpp"
+
+#include
+#include
+#include
+#include
+
+constexpr double defaultTempo = double(120);
+
+inline double calcOscillatorPitch(double octave, double cent)
+{
+ return std::exp2(octave - octaveOffset + cent / 1200.0);
+}
+
+inline double calcPitch(double semitone, double equalTemperament = 12.0)
+{
+ return std::exp2(semitone / equalTemperament);
+}
+
+template
+inline auto prepareSerialAllpassTime(double upRate, double allpassMaxTimeHz, Rng &rng)
+{
+ std::array delaySamples{};
+ const auto scaler = std::max(
+ double(0), std::ceil(upRate * nAllpass / allpassMaxTimeHz) - double(2) * nAllpass);
+ double sumSamples = 0;
+ std::uniform_real_distribution dist{double(0), double(1)};
+ for (size_t idx = 0; idx < nAllpass; ++idx) {
+ delaySamples[idx] = dist(rng);
+ sumSamples += delaySamples[idx];
+ }
+ double sumFraction = 0;
+ for (size_t idx = 0; idx < nAllpass; ++idx) {
+ const auto samples = double(2) + scaler * delaySamples[idx] / sumSamples;
+ delaySamples[idx] = std::floor(samples);
+ sumFraction += samples - delaySamples[idx];
+ }
+ delaySamples[0] += std::round(sumFraction);
+ return delaySamples;
+}
+
+constexpr size_t nPitch = 8;
+constexpr std::array pitchHarmonicPlus12{
+ double(1), double(4), double(5), double(12),
+ double(13), double(15), double(16), double(24),
+};
+constexpr std::array pitchHarmonicTimes5{
+ double(1), double(5), double(8), double(10),
+ double(15), double(16), double(20), double(24),
+};
+constexpr std::array pitchSemitone1_2_7_9{
+ double(1), double(8) / double(7), double(3) / double(2), double(5) / double(3),
+ double(1), double(8) / double(7), double(3) / double(2), double(5) / double(3),
+};
+constexpr std::array pitchCircularMembraneMode{
+ double(1.00000000000000), double(1.593340505695112), double(2.1355487866494034),
+ double(2.295417267427694), double(2.6530664045492145), double(2.9172954551172228),
+ double(3.155464815408362), double(3.5001474903090264),
+};
+constexpr std::array pitchPrimeNumber{
+ double(2) / double(2), double(3) / double(2), double(5) / double(2),
+ double(7) / double(2), double(11) / double(2), double(13) / double(2),
+ double(17) / double(2), double(19) / double(2),
+};
+
+enum PitchTypeName : size_t {
+ harmonic,
+ harmonicPlus12,
+ harmonicTimes5,
+ harmonicCycle1_5,
+ harmonicOdd,
+ semitone1_2_7_9,
+ circularMembraneMode,
+ primeNumber,
+ octave,
+};
+
+inline double pitchFunc(size_t pitchType, size_t index)
+{
+ index %= size_t(8);
+ if (pitchType == harmonic) {
+ return double(index + 1);
+ } else if (pitchType == harmonicPlus12) {
+ return pitchHarmonicPlus12[index];
+ } else if (pitchType == harmonicTimes5) {
+ return pitchHarmonicTimes5[index];
+ } else if (pitchType == harmonicCycle1_5) {
+ return (index & 1) == 0 ? double(1) : double(5);
+ } else if (pitchType == harmonicOdd) {
+ return double(2 * index + 1);
+ } else if (pitchType == semitone1_2_7_9) {
+ return pitchSemitone1_2_7_9[index];
+ } else if (pitchType == circularMembraneMode) {
+ return pitchCircularMembraneMode[index];
+ } else if (pitchType == primeNumber) {
+ return pitchPrimeNumber[index];
+ }
+ return double(size_t(1) << index); // `pitchType == octave`.
+}
+
+template
+inline double pitchRatio(double pitch, double spread, double rndCent, Rng &rng)
+{
+ const auto rndRange = rndCent / double(1200);
+ std::uniform_real_distribution dist{-rndRange, rndRange};
+ return std::lerp(double(1), pitch, spread) * std::exp2(dist(rng));
+}
+
+void DSPCore::setup(double sampleRate)
+{
+ noteStack.reserve(1024);
+ noteStack.resize(0);
+
+ this->sampleRate = sampleRate;
+ upRate = sampleRate * upFold;
+
+ SmootherCommon::setTime(double(0.2));
+
+ const auto maxDelayTimeSamples = upRate;
+ noiseAllpass.setup(maxDelayTimeSamples);
+ wireAllpass.setup(maxDelayTimeSamples);
+ wireEnergyDecay.setup(upRate * double(0.001));
+ membrane1EnergyDecay.setup(upRate * double(0.001));
+ membrane2EnergyDecay.setup(upRate * double(0.001));
+ membrane1.setup(maxDelayTimeSamples);
+ membrane2.setup(maxDelayTimeSamples);
+
+ reset();
+ startup();
+}
+
+#define ASSIGN_PARAMETER(METHOD) \
+ using ID = ParameterID::ID; \
+ const auto &pv = param.value; \
+ \
+ pitchSmoothingKp \
+ = EMAFilter::secondToP(upRate, pv[ID::noteSlideTimeSecond]->getDouble()); \
+ auto pitchBend \
+ = calcPitch(pv[ID::pitchBendRange]->getDouble() * pv[ID::pitchBend]->getDouble()); \
+ auto notePitch = calcNotePitch(pitchBend * noteNumber); \
+ interpPitch.METHOD(notePitch); \
+ \
+ wireDistance.METHOD(pv[ID::wireDistance]->getDouble()); \
+ wireCollisionTypeMix.METHOD(pv[ID::wireCollisionTypeMix]->getDouble()); \
+ impactWireMix.METHOD(pv[ID::impactWireMix]->getDouble()); \
+ secondaryDistance.METHOD(pv[ID::secondaryDistance]->getDouble()); \
+ crossFeedbackGain.METHOD(pv[ID::crossFeedbackGain]->getDouble()); \
+ delayTimeModAmount.METHOD( \
+ pv[ID::delayTimeModAmount]->getDouble() * upRate / double(48000)); \
+ secondaryFdnMix.METHOD(pv[ID::secondaryFdnMix]->getDouble()); \
+ membraneWireMix.METHOD(pv[ID::membraneWireMix]->getDouble()); \
+ outputGain.METHOD(pv[ID::outputGain]->getDouble()); \
+ \
+ safetyHighpass.METHOD( \
+ pv[ID::safetyHighpassHz]->getDouble() / sampleRate, \
+ std::numbers::sqrt2_v / double(2)); \
+ \
+ noiseLowpass.METHOD(pv[ID::noiseLowpassHz]->getDouble() / upRate); \
+ paramRng.seed(pv[ID::seed]->getInt()); \
+ noiseAllpass.timeInSamples.METHOD( \
+ prepareSerialAllpassTime( \
+ upRate, pv[ID::noiseAllpassMaxTimeHz]->getDouble(), paramRng)); \
+ wireAllpass.timeInSamples.METHOD( \
+ prepareSerialAllpassTime( \
+ upRate, pv[ID::wireFrequencyHz]->getDouble(), paramRng)); \
+ \
+ 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 secondaryPitchOffset = pv[ID::secondaryPitchOffset]->getDouble(); \
+ const auto delayTimeFreq1 = pv[ID::delayTimeHz]->getDouble() / upRate; \
+ const auto delayTimeFreq2 = delayTimeFreq1 * std::exp2(secondaryPitchOffset); \
+ const auto bandpassCutRatio = std::exp2(pv[ID::bandpassCutRatio]->getDouble()); \
+ const auto secondaryQOffset = pv[ID::secondaryQOffset]->getDouble(); \
+ const auto delayTimeSpread = pv[ID::delayTimeSpread]->getDouble(); \
+ const auto bandpassCutSpread = pv[ID::bandpassCutSpread]->getDouble(); \
+ const auto pitchRandomCent = pv[ID::pitchRandomCent]->getDouble(); \
+ const size_t pitchType = pv[ID::pitchType]->getInt(); \
+ for (size_t idx = 0; idx < maxFdnSize; ++idx) { \
+ const auto pitch = pitchFunc(pitchType, idx); \
+ \
+ const auto delayCutRatio1 \
+ = pitchRatio(pitch, delayTimeSpread, pitchRandomCent, paramRng); \
+ membrane1.delayTimeSamples[idx] = double(1) / (delayTimeFreq1 * delayCutRatio1); \
+ \
+ const auto bpCutRatio1 = bandpassCutRatio \
+ * pitchRatio(pitch, bandpassCutSpread, pitchRandomCent, paramRng); \
+ membrane1.bandpassCutoff.METHOD##At(idx, delayTimeFreq1 *bpCutRatio1); \
+ \
+ const auto delayCutRatio2 \
+ = pitchRatio(pitch, delayTimeSpread, pitchRandomCent, paramRng); \
+ membrane2.delayTimeSamples[idx] = double(1) / (delayTimeFreq2 * delayCutRatio2); \
+ \
+ const auto bpCutRatio2 = bandpassCutRatio \
+ * pitchRatio(pitch, bandpassCutSpread, pitchRandomCent, paramRng); \
+ membrane2.bandpassCutoff.METHOD##At(idx, delayTimeFreq2 *bpCutRatio2); \
+ } \
+ const auto bandpassQ = pv[ID::bandpassQ]->getDouble(); \
+ membrane1.bandpassQ.METHOD(bandpassQ); \
+ membrane2.bandpassQ.METHOD( \
+ std::clamp(bandpassQ *std::exp2(secondaryQOffset), double(0.1), double(100)));
+
+void DSPCore::updateUpRate()
+{
+ upRate = sampleRate * fold[overSampling];
+ SmootherCommon::setSampleRate(upRate);
+ membrane1.onSampleRateChange(upRate);
+ membrane2.onSampleRateChange(upRate);
+}
+
+void DSPCore::reset()
+{
+ overSampling = param.value[ParameterID::ID::overSampling]->getInt();
+ updateUpRate();
+
+ ASSIGN_PARAMETER(reset);
+
+ startup();
+
+ noteNumber = 69.0;
+ velocity = 0;
+
+ noiseGain = 0;
+ noiseDecay = 0;
+ noiseAllpass.reset();
+
+ wireAllpass.reset();
+ wireEnergyDecay.reset();
+ wireEnergyNoise.reset();
+ wirePosition = 0;
+ wireVelocity = 0;
+ wireGain = 0;
+ wireDecay = 0;
+
+ envelope.reset();
+ releaseSmoother.reset();
+
+ membrane1Position = 0;
+ membrane1Velocity = 0;
+ membrane2Position = 0;
+ membrane2Velocity = 0;
+ membrane1EnergyDecay.reset();
+ membrane2EnergyDecay.reset();
+ membrane1.reset();
+ membrane2.reset();
+
+ halfbandIir.reset();
+}
+
+void DSPCore::startup()
+{
+ using ID = ParameterID::ID;
+ const auto &pv = param.value;
+ noiseRng.seed(pv[ID::seed]->getInt());
+}
+
+void DSPCore::setParameters()
+{
+ size_t newOverSampling = param.value[ParameterID::ID::overSampling]->getInt();
+ if (overSampling != newOverSampling) {
+ overSampling = newOverSampling;
+ updateUpRate();
+ }
+ ASSIGN_PARAMETER(push);
+}
+
+// Overwrites `p0` and `p1`.
+inline void solveCollision(double &p0, double &p1, double v0, double v1, double distance)
+{
+ const auto diff = p0 - p1 + distance;
+ if (diff >= 0) {
+ p0 = 0;
+ p1 = 0;
+ return;
+ }
+
+ auto sum = -diff;
+ const auto r0 = std::abs(v0);
+ const auto r1 = std::abs(v1);
+ if (r0 + r1 >= std::numeric_limits::epsilon()) sum /= r0 + r1;
+ p0 = sum * r1;
+ p1 = -sum * r0;
+}
+
+double DSPCore::processSample()
+{
+ wireDistance.process();
+ wireCollisionTypeMix.process();
+ impactWireMix.process();
+ secondaryDistance.process();
+ const auto crossGain = crossFeedbackGain.process();
+ const auto timeModAmt = delayTimeModAmount.process();
+ secondaryFdnMix.process();
+ membraneWireMix.process();
+ outputGain.process();
+
+ constexpr auto eps = std::numeric_limits::epsilon();
+ double sig = 0;
+ if (noiseGain > eps) {
+ std::uniform_real_distribution dist{double(-1), double(1)};
+ const auto noise = noiseGain * dist(noiseRng);
+ noiseGain *= noiseDecay;
+ sig += noiseLowpass.process(noise);
+ }
+ sig = std::tanh(noiseAllpass.process(sig, double(0.95)));
+
+ solveCollision(
+ wirePosition, membrane1Position, wireVelocity, membrane1Velocity,
+ wireDistance.getValue());
+
+ // TODO: Send wire-membrane1 collision status to GUI.
+
+ auto wireCollision = std::lerp(
+ wireEnergyNoise.process(wirePosition, noiseRng),
+ wireEnergyDecay.process(wirePosition), wireCollisionTypeMix.getValue());
+ wireCollision = double(8) * std::tanh(double(0.125) * wireCollision);
+ const auto wireIn = double(0.995) * (sig + wireCollision);
+ const auto wirePos = wireAllpass.process(wireIn, double(0.5)) * wireGain;
+ wireGain *= wireDecay;
+ wireVelocity = wirePos - wirePosition;
+ wirePosition = wirePos;
+
+ const auto wireOut = std::lerp(sig, wirePosition, impactWireMix.getValue());
+ sig = wireOut;
+
+ solveCollision(
+ membrane1Position, membrane2Position, membrane1Velocity, membrane2Velocity,
+ secondaryDistance.getValue());
+
+ // TODO: Send membrane1-membrane2 collision status to GUI.
+
+ const auto env = std::exp2(envelope.process() + releaseSmoother.process());
+ const auto pitch = env * interpPitch.process(pitchSmoothingKp);
+
+ const auto collision1 = membrane1EnergyDecay.process(membrane1Position);
+ const auto p1 = membrane1.process(sig, crossGain, pitch, timeModAmt, feedbackMatrix);
+ membrane1Velocity = p1 - membrane1Position;
+ membrane1Position = p1;
+
+ const auto collision2 = membrane2EnergyDecay.process(membrane2Position);
+ const auto p2 = membrane2.process(sig, crossGain, pitch, timeModAmt, feedbackMatrix);
+ membrane2Velocity = p2 - membrane2Position;
+ membrane2Position = p2;
+
+ sig = std::lerp(p1, p2, secondaryFdnMix.getValue());
+ sig = std::lerp(sig, wireOut, membraneWireMix.getValue());
+
+ return outputGain.getValue() * sig;
+}
+
+void DSPCore::process(const size_t length, float *out0, float *out1)
+{
+ ScopedNoDenormals scopedDenormals;
+
+ using ID = ParameterID::ID;
+ const auto &pv = param.value;
+
+ SmootherCommon::setBufferSize(double(length));
+ SmootherCommon::setSampleRate(upRate);
+
+ bool isSafetyHighpassEnabled = pv[ID::safetyHighpassEnable]->getInt();
+
+ std::array halfbandInput{};
+ for (size_t i = 0; i < length; ++i) {
+ processMidiNote(i);
+
+ if (overSampling) {
+ for (size_t j = 0; j < upFold; ++j) halfbandInput[j] = processSample();
+ auto sig = float(halfbandIir.process(halfbandInput));
+ if (isSafetyHighpassEnabled) sig = safetyHighpass.process(sig);
+ out0[i] = sig;
+ out1[i] = sig;
+ } else {
+ auto sig = float(processSample());
+ if (isSafetyHighpassEnabled) sig = safetyHighpass.process(sig);
+ out0[i] = sig;
+ out1[i] = sig;
+ }
+ }
+}
+
+void DSPCore::noteOn(NoteInfo &info)
+{
+ using ID = ParameterID::ID;
+ auto &pv = param.value;
+
+ constexpr auto eps = std::numeric_limits::epsilon();
+
+ noteStack.push_back(info);
+
+ noteNumber = info.noteNumber;
+ auto notePitch = calcNotePitch(info.noteNumber);
+ auto pitchBend
+ = calcPitch(pv[ID::pitchBendRange]->getDouble() * pv[ID::pitchBend]->getDouble());
+ if (pv[ID::slideAtNoteOn]->getInt()) {
+ interpPitch.push(notePitch);
+ } else {
+ interpPitch.reset(notePitch);
+ }
+
+ velocity = velocityMap.map(info.velocity);
+
+ noiseGain = velocity;
+ noiseDecay = std::pow(
+ double(1e-3), double(1) / (upRate * pv[ID::noiseDecaySeconds]->getDouble()));
+
+ wireGain = double(2);
+ wireDecay = std::pow(eps, double(1) / (upRate * pv[ID::wireDecaySeconds]->getDouble()));
+
+ releaseSmoother.prepare(envelope.process(), double(0.002) * upRate);
+ envelope.noteOn(
+ pv[ID::envelopeModAmount]->getDouble(),
+ pv[ID::envelopeAttackSeconds]->getDouble() * upRate,
+ pv[ID::envelopeDecaySeconds]->getDouble() * upRate);
+
+ const auto crossFeedbackGain = pv[ID::crossFeedbackGain]->getDouble();
+ membrane1.noteOn();
+ membrane2.noteOn();
+}
+
+void DSPCore::noteOff(int_fast32_t noteId)
+{
+ using ID = ParameterID::ID;
+ auto &pv = param.value;
+
+ auto it = std::find_if(noteStack.begin(), noteStack.end(), [&](const NoteInfo &info) {
+ return info.id == noteId;
+ });
+ if (it == noteStack.end()) return;
+ noteStack.erase(it);
+
+ if (!noteStack.empty() && pv[ID::slideAtNoteOff]->getInt()) {
+ noteNumber = noteStack.back().noteNumber;
+ interpPitch.push(calcNotePitch(noteNumber));
+ }
+}
+
+double DSPCore::calcNotePitch(double note)
+{
+ using ID = ParameterID::ID;
+ auto &pv = param.value;
+
+ auto semitone = pv[ID::tuningSemitone]->getInt() - double(semitoneOffset + 57);
+ auto cent = pv[ID::tuningCent]->getDouble() / double(100);
+ auto equalTemperament = pv[ID::tuningET]->getInt() + 1;
+ return std::exp2((note + semitone + cent) / equalTemperament);
+}
diff --git a/GenericDrum/source/dsp/dspcore.hpp b/GenericDrum/source/dsp/dspcore.hpp
new file mode 100644
index 00000000..5f5d52e3
--- /dev/null
+++ b/GenericDrum/source/dsp/dspcore.hpp
@@ -0,0 +1,156 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "../../../common/dsp/constants.hpp"
+#include "../../../common/dsp/multirate.hpp"
+#include "../../../common/dsp/smoother.hpp"
+#include "../parameter.hpp"
+#include "envelope.hpp"
+#include "filter.hpp"
+
+#include
+
+using namespace SomeDSP;
+using namespace Steinberg::Synth;
+
+class DSPCore {
+public:
+ struct NoteInfo {
+ bool isNoteOn;
+ uint32_t frame;
+ int32_t id;
+ float noteNumber;
+ float velocity;
+ };
+
+ DSPCore()
+ {
+ midiNotes.reserve(1024);
+ noteStack.reserve(1024);
+ }
+
+ GlobalParameter param;
+ bool isPlaying = false;
+ double tempo = 120.0;
+ double beatsElapsed = 0.0;
+ double timeSigUpper = 1.0;
+ double timeSigLower = 4.0;
+
+ void setup(double sampleRate);
+ void reset();
+ void startup();
+ void setParameters();
+ void process(const size_t length, float *out0, float *out1);
+ void noteOn(NoteInfo &info);
+ void noteOff(int_fast32_t noteId);
+
+ void pushMidiNote(
+ bool isNoteOn,
+ uint32_t frame,
+ int32_t noteId,
+ int16_t noteNumber,
+ float tuning,
+ float velocity)
+ {
+ NoteInfo note;
+ note.isNoteOn = isNoteOn;
+ note.frame = frame;
+ note.id = noteId;
+ note.noteNumber = noteNumber + tuning;
+ note.velocity = velocity;
+ midiNotes.push_back(note);
+ }
+
+ void processMidiNote(size_t frame)
+ {
+ while (true) {
+ auto it = std::find_if(midiNotes.begin(), midiNotes.end(), [&](const NoteInfo &nt) {
+ return nt.frame == frame;
+ });
+ if (it == std::end(midiNotes)) return;
+ if (it->isNoteOn)
+ noteOn(*it);
+ else
+ noteOff(it->id);
+ midiNotes.erase(it);
+ }
+ }
+
+private:
+ std::vector midiNotes;
+ std::vector noteStack;
+
+ DecibelScale velocityMap{-60, 0, true};
+ DecibelScale velocityToCouplingDecayMap{-40, 0, false};
+ double velocity = 0;
+
+ static constexpr size_t upFold = 2;
+ static constexpr std::array fold{1, upFold};
+ size_t overSampling = 2;
+ double sampleRate = 44100.0;
+ double upRate = upFold * 44100.0;
+
+ double noteNumber = 69.0;
+ double pitchSmoothingKp = 1.0;
+ ExpSmootherLocal interpPitch;
+
+ ExpSmoother wireDistance;
+ ExpSmoother wireCollisionTypeMix;
+ ExpSmoother impactWireMix;
+ ExpSmoother secondaryDistance;
+ ExpSmoother crossFeedbackGain;
+ ExpSmoother delayTimeModAmount;
+ ExpSmoother secondaryFdnMix;
+ ExpSmoother membraneWireMix;
+ ExpSmoother outputGain;
+
+ std::minstd_rand noiseRng{0};
+ std::minstd_rand paramRng{0};
+ double noiseGain = 0;
+ double noiseDecay = 0;
+ ComplexLowpass noiseLowpass;
+ SerialAllpass noiseAllpass;
+
+ SerialAllpass wireAllpass;
+ EnergyStoreDecay wireEnergyDecay;
+ EnergyStoreNoise wireEnergyNoise;
+ double wirePosition = 0;
+ double wireVelocity = 0;
+ double wireGain = 0;
+ double wireDecay = 0;
+
+ DoubleEmaADEnvelope envelope;
+ TransitionReleaseSmoother releaseSmoother;
+ FeedbackMatrix feedbackMatrix;
+ double membrane1Position = 0;
+ double membrane1Velocity = 0;
+ double membrane2Position = 0;
+ double membrane2Velocity = 0;
+ EnergyStoreDecay membrane1EnergyDecay;
+ EnergyStoreDecay membrane2EnergyDecay;
+ EasyFDN membrane1;
+ EasyFDN membrane2;
+
+ HalfBandIIR> halfbandIir;
+ SVFHighpass safetyHighpass;
+
+ void updateUpRate();
+ double calcNotePitch(double note);
+ double processSample();
+};
diff --git a/GenericDrum/source/dsp/envelope.hpp b/GenericDrum/source/dsp/envelope.hpp
new file mode 100644
index 00000000..537ad3a7
--- /dev/null
+++ b/GenericDrum/source/dsp/envelope.hpp
@@ -0,0 +1,136 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "../../../common/dsp/smoother.hpp"
+#include "../../../common/dsp/solver.hpp"
+
+#include
+#include
+#include
+#include
+
+namespace SomeDSP {
+
+// Non-recursive form of DoubleEMAEnvelope output. Negated because `minimizeScalarBrent`
+// finds minimum.
+template T doubleEmaEnvelopeD0Negative(T n, T k_A, T k_D)
+{
+ auto A = std::pow(T(1) - k_A, n + T(1)) * (k_A * n + k_A + T(1));
+ auto D = std::pow(T(1) - k_D, n + T(1)) * (k_D * n + k_D + T(1));
+ return (A - T(1)) * D;
+}
+
+template T samplesToKp(T timeInSamples)
+{
+ if (timeInSamples < std::numeric_limits::epsilon()) return T(1);
+ auto y = T(1) - std::cos(T(twopi) / timeInSamples);
+ return -y + std::sqrt(y * (y + T(2)));
+}
+
+template class DoubleEmaADEnvelope {
+private:
+ Sample v1_A = 0;
+ Sample v2_A = 0;
+
+ Sample v1_D = 0;
+ Sample v2_D = 0;
+
+ Sample k_A = Sample(1);
+ Sample k_D = Sample(1);
+
+ Sample gain = 0; // Gain to normalize peak to 1.
+
+ inline void initState()
+ {
+ v1_A = 0;
+ v2_A = 0;
+ v1_D = Sample(1);
+ v2_D = Sample(1);
+ }
+
+public:
+ void reset()
+ {
+ initState();
+ gain = 0;
+ }
+
+ void noteOn(Sample targetAmplitude, Sample attackTimeSamples, Sample decayTimeSamples)
+ {
+ // Using `double` for minimization. `float` is inaccurate over 10^4 samples.
+ auto kA = samplesToKp(attackTimeSamples);
+ auto kD = samplesToKp(decayTimeSamples);
+
+ if (kA == 1.0 || kD == 1.0) {
+ gain = Sample(1);
+ k_A = Sample(kA);
+ k_D = Sample(kD);
+ } else {
+ auto result = minimizeScalarBrent(
+ [&](double n) { return doubleEmaEnvelopeD0Negative(n, kA, kD); });
+
+ auto peak = -result.second;
+ gain
+ = peak < std::numeric_limits::epsilon() ? Sample(1) : Sample(1.0 / peak);
+ k_A = Sample(kA);
+ k_D = Sample(kD);
+ }
+ gain *= targetAmplitude;
+
+ initState();
+ }
+
+ Sample process()
+ {
+ v1_A += k_A * (Sample(1) - v1_A);
+ v2_A += k_A * (v1_A - v2_A);
+
+ v1_D += k_D * (Sample(0) - v1_D);
+ v2_D += k_D * (v1_D - v2_D);
+
+ return gain * v2_A * v2_D;
+ }
+};
+
+/**
+Use with DoubleEmaADEnvelope.
+
+When note-on comes in, and DoubleEmaADEnvelope has to reset, move the current value of
+DoubleEmaADEnvelope to TransitionReleaseSmoother using `prepare()`. `process()` output of
+DoubleEmaADEnvelope and TransitionReleaseSmoother are summed together.
+*/
+template class TransitionReleaseSmoother {
+private:
+ Sample v0 = 0;
+ Sample decay = 0;
+
+public:
+ void reset() { v0 = 0; }
+
+ // decaySamples = sampleRate * seconds.
+ void prepare(Sample value, Sample decaySamples)
+ {
+ v0 += value;
+ decay = std::pow(std::numeric_limits::epsilon(), Sample(1) / decaySamples);
+ }
+
+ Sample process() { return v0 *= decay; }
+};
+
+} // namespace SomeDSP
diff --git a/GenericDrum/source/dsp/filter.hpp b/GenericDrum/source/dsp/filter.hpp
new file mode 100644
index 00000000..e467c3e9
--- /dev/null
+++ b/GenericDrum/source/dsp/filter.hpp
@@ -0,0 +1,529 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "../../../common/dsp/smoother.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace SomeDSP {
+
+template class ComplexLowpass {
+private:
+ Sample x1 = 0;
+ std::complex y1{};
+ ExpSmoother> b{};
+ ExpSmoother> a1{};
+
+ inline Sample setR(Sample cut, Sample lowR, Sample highR, Sample lowCut, Sample highCut)
+ {
+ if (cut <= lowCut) return lowR;
+ if (cut >= highCut) return highR;
+ return lowR + (highR - lowR) * (cut - lowCut) / (highCut - lowCut);
+ }
+
+public:
+ void push(Sample freqNormalized)
+ {
+ auto cut = std::exp(std::complex{
+ Sample(0), Sample(2) * std::numbers::pi_v * freqNormalized});
+ auto R = setR(freqNormalized, Sample(0.25), Sample(0.9), Sample(0.01), Sample(0.2));
+ a1.push(cut * std::pow(R, cut.imag()));
+ b.push((Sample(1) - a1.target) / Sample(2));
+ }
+
+ /* freqNormalized in [0, 0.5). R in [0, 1]. */
+ void reset(Sample freqNormalized)
+ {
+ x1 = 0;
+ y1 = std::complex{Sample(0), Sample(0)};
+
+ push(freqNormalized);
+ a1.catchUp();
+ b.catchUp();
+ }
+
+ Sample process(Sample x0)
+ {
+ y1 = b.process() * (x0 + x1) + a1.process() * y1;
+ x1 = x0;
+ return y1.real();
+ }
+};
+
+template class ParallelMatchedBandpass {
+private:
+ static constexpr Sample minCutoff = Sample(0.00001);
+ static constexpr Sample nyquist = Sample(0.49998);
+
+ std::array x1{};
+ std::array x2{};
+ std::array y1{};
+ std::array y2{};
+
+public:
+ void reset()
+ {
+ x1.fill({});
+ x2.fill({});
+ y1.fill({});
+ y2.fill({});
+ }
+
+ void process(
+ std::array &x0,
+ const std::array &cutoffNormalized,
+ Sample Q,
+ Sample mod)
+ {
+ using T = Sample;
+
+ const auto q = T(0.5) / Q;
+ for (size_t idx = 0; idx < length; ++idx) {
+ constexpr Sample twopi = T(2) * std::numbers::pi_v;
+ const auto w0 = twopi * std::clamp(cutoffNormalized[idx] * mod, minCutoff, nyquist);
+
+ auto a1 = T(-2) * std::exp(-q * w0)
+ * (q <= T(1) ? std::cos(std::sqrt(T(1) - q * q) * w0)
+ : std::cosh(std::sqrt(q * q - T(1)) * w0));
+ auto a2 = std::exp(T(-2) * q * w0);
+
+ auto r0 = (T(1) + a1 + a2) / (w0 * Q);
+ auto wQ = w0 / Q;
+ auto one_ww = T(1) - w0 * w0;
+ auto r1 = wQ * (T(1) - a1 + a2) / std::sqrt(one_ww * one_ww + wQ * wQ);
+
+ auto b0 = T(0.5) * r0 + T(0.25) * r1;
+ auto b1 = T(-0.5) * r1;
+ auto b2 = -b0 - b1;
+
+ auto y0 = b0 * x0[idx] + b1 * x1[idx] + b2 * x2[idx] - a1 * y1[idx] - a2 * y2[idx];
+
+ x2[idx] = x1[idx];
+ x1[idx] = x0[idx];
+ y2[idx] = y1[idx];
+ y1[idx] = y0;
+
+ x0[idx] = y0;
+ }
+ }
+};
+
+// This implementation only updates internal values at control rate.
+template class SVFHighpass {
+private:
+ static constexpr Sample minCutoff = Sample(0.00001);
+ static constexpr Sample nyquist = Sample(0.49998);
+
+ Sample s1 = 0;
+ Sample s2 = 0;
+
+ ExpSmoother g;
+ ExpSmoother d;
+ ExpSmoother k;
+
+public:
+ void push(Sample freqNormalized, Sample Q)
+ {
+ g.push(std::tan(
+ std::numbers::pi_v * std::clamp(freqNormalized, minCutoff, nyquist)));
+ k.push(Sample(1) / Q);
+ d.push(Sample(1) / (Sample(1) + g.target * (g.target + k.target)));
+ }
+
+ void reset(Sample freqNormalized, Sample Q)
+ {
+ s1 = 0;
+ s2 = 0;
+
+ push(freqNormalized, Q);
+ g.catchUp();
+ k.catchUp();
+ d.catchUp();
+ }
+
+ Sample process(Sample v0)
+ {
+ g.process();
+ d.process();
+ k.process();
+ auto v1 = (s1 + g.value * (v0 - s2)) * d.value;
+ auto v2 = s2 + g.value * v1;
+ s1 = Sample(2) * v1 - s1;
+ s2 = Sample(2) * v2 - s2;
+ return v0 - k.value * v1 - v2;
+ }
+};
+
+template class Delay {
+private:
+ int wptr = 0;
+ std::vector buf{Sample(0), Sample(0)};
+
+public:
+ void setup(Sample maxTimeSamples)
+ {
+ buf.resize(std::max(size_t(2), size_t(maxTimeSamples) + 1));
+ reset();
+ }
+
+ void reset() { std::fill(buf.begin(), buf.end(), Sample(0)); }
+
+ 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.
+ Sample clamped = std::clamp(timeInSamples, Sample(1), Sample(size - 1));
+ const int timeInt = int(clamped);
+ Sample rFraction = clamped - Sample(timeInt);
+
+ // Write to buffer.
+ buf[wptr] = input;
+ if (++wptr >= size) wptr = 0;
+
+ // Read from buffer.
+ int rptr0 = wptr - timeInt;
+ if (rptr0 < 0) rptr0 += size;
+ return std::lerp(buf[rptr0], buf[(rptr0 != 0 ? rptr0 : size) - 1], rFraction);
+ }
+};
+
+template class ParallelRateLimiter {
+public:
+ static constexpr Sample rate = Sample(0.5); // Fixed for delays.
+
+ std::array value{};
+ std::array target{};
+
+ void pushAt(size_t index, Sample pushedValue = 0) { target[index] = pushedValue; }
+ void push(const std::array &pushedValue) { target = pushedValue; }
+
+ void resetAt(size_t index, Sample resetValue = 0)
+ {
+ target[index] = resetValue;
+ value[index] = resetValue;
+ }
+
+ void reset(const std::array &resetValue)
+ {
+ target = resetValue;
+ value = resetValue;
+ }
+
+ void process()
+ {
+ for (size_t idx = 0; idx < length; ++idx) {
+ auto diff = target[idx] - value[idx];
+ value[idx]
+ = std::abs(diff) > rate ? value[idx] + std::copysign(rate, diff) : target[idx];
+ }
+ }
+};
+
+template class SerialAllpass {
+private:
+ std::array buffer{};
+ std::array, nAllpass> delay;
+
+public:
+ static constexpr size_t size = nAllpass;
+ ParallelRateLimiter timeInSamples;
+
+ 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 gain)
+ {
+ timeInSamples.process();
+
+ Sample sum = input;
+ for (size_t idx = 0; idx < nAllpass; ++idx) {
+ const auto x0 = input - gain * buffer[idx];
+ input = buffer[idx] + gain * x0;
+ sum += input;
+ buffer[idx] = delay[idx].process(x0, timeInSamples.value[idx]);
+ }
+ return sum;
+ }
+};
+
+template class EnergyStoreDecay {
+private:
+ static constexpr Sample eps = std::numeric_limits::epsilon();
+ Sample sum = 0;
+ Sample decay = 0;
+ Sample gain = 0;
+
+public:
+ void setup(Sample decayTimeSamples)
+ {
+ sum = 0;
+ decay = -std::log(eps) / decayTimeSamples;
+ gain = std::exp(-decay);
+ }
+
+ void reset() { sum = 0; }
+
+ Sample process(Sample value)
+ {
+ const auto absed = std::abs(value);
+ if (absed > eps) sum = (sum + value) * decay;
+ return sum *= gain;
+ }
+};
+
+template class EnergyStoreNoise {
+private:
+ Sample sum = 0;
+
+public:
+ void reset() { sum = 0; }
+
+ Sample process(Sample value, Rng &rng)
+ {
+ sum += std::abs(value);
+ std::uniform_real_distribution dist{Sample(-sum), Sample(sum)};
+ const auto out = dist(rng);
+ sum -= std::abs(out);
+ return out;
+ }
+};
+
+template class ParallelDelay {
+private:
+ std::array wptr{};
+ std::array, length> buffer;
+
+public:
+ ParallelDelay()
+ {
+ for (auto &bf : buffer) bf.resize(2);
+ }
+
+ void setup(Sample maxTimeSamples)
+ {
+ auto maxSize = std::max(size_t(2), size_t(maxTimeSamples) + 1);
+ for (auto &bf : buffer) bf.resize(maxSize);
+
+ reset();
+ }
+
+ void reset()
+ {
+ wptr.fill({});
+ for (auto &bf : buffer) std::fill(bf.begin(), bf.end(), Sample(0));
+ }
+
+ 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.
+ Sample clamped
+ = std::clamp(timeInSamples[idx] / timeScaler, Sample(1), Sample(size - 1));
+ int timeInt = int(clamped);
+ Sample rFraction = clamped - Sample(timeInt);
+
+ // 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] = std::lerp(bf[rptr0], bf[(rptr0 != 0 ? rptr0 : size) - 1], rFraction);
+ }
+ }
+};
+
+template class MembraneDelayRateLimiter {
+private:
+ static constexpr Sample rate = Sample(0.5);
+
+public:
+ std::array value{};
+
+ void process(
+ const std::array &base,
+ const std::array &modIn,
+ Sample modAmount)
+ {
+ for (size_t idx = 0; idx < length; ++idx) {
+ auto target = base[idx] - std::abs(modAmount * modIn[idx]);
+ auto diff = target - value[idx];
+ value[idx]
+ = std::abs(diff) > rate ? value[idx] + std::copysign(rate, diff) : target;
+ }
+ }
+};
+
+template class FeedbackMatrix {
+public:
+ std::array seed{};
+ std::array, length> matrix;
+
+ // Construct Householder matrix. Call this after updating `seed`.
+ //
+ // `matrix` is 2D array of a square matrix.
+ // `seed` is 1D array of a vector which length is the same as `matrix`.
+ //
+ // Reference: https://nhigham.com/2020/09/15/what-is-a-householder-matrix/
+ void constructHouseholder()
+ {
+ Sample denom = 0;
+ for (size_t i = 0; i < length; ++i) denom += seed[i] * seed[i];
+
+ if (denom <= std::numeric_limits::epsilon()) {
+ for (size_t i = 0; i < length; ++i) {
+ for (size_t j = 0; j < length; ++j) {
+ matrix[i].target[j] = i == j ? Sample(1) : Sample(0);
+ }
+ }
+ return;
+ }
+
+ auto scale = Sample(-2) / denom;
+
+ for (size_t i = 0; i < length; ++i) {
+ // Diagonal elements.
+ matrix[i].target[i] = Sample(1) + scale * seed[i] * seed[i];
+
+ // Non-diagonal elements.
+ for (size_t j = i + 1; j < length; ++j) {
+ auto value = scale * seed[i] * seed[j];
+ matrix[i].target[j] = value;
+ matrix[j].target[i] = value;
+ }
+ }
+ }
+
+ void reset()
+ {
+ for (auto &x : matrix) x.catchUp();
+ }
+
+ void process()
+ {
+ for (auto &x : matrix) x.process();
+ }
+
+ Sample at(size_t i, size_t j) { return matrix[i].value[j]; }
+};
+
+template class EasyFDN {
+private:
+ size_t bufIndex = 0;
+ std::array, 2> buf;
+
+ MembraneDelayRateLimiter delayTimeRateLimiter;
+ ParallelDelay delay;
+ ParallelMatchedBandpass bandpass;
+
+ Sample safetyGain = 0;
+ Sample crossDecaySteep = 0;
+ Sample crossDecayGentle = 0;
+
+public:
+ // Before calling `process`,
+ // 1. Fill all the parameters in the paragraph below.
+ // 2. Call `constructHouseholder`.
+ // 3. Call `reset`, but only when resetting.
+ std::array delayTimeSamples{};
+ ParallelExpSmoother bandpassCutoff; // Normalized in [0, 0.5).
+ ExpSmoother bandpassQ;
+ ExpSmoother crossGain;
+
+ void setup(Sample maxTimeSamples) { delay.setup(maxTimeSamples); }
+
+ void onSampleRateChange(Sample sampleRate)
+ {
+ crossDecaySteep = std::pow(Sample(0.85), Sample(48000) / sampleRate);
+ crossDecayGentle
+ = std::pow(std::numeric_limits::epsilon(), Sample(0.366) / sampleRate);
+ }
+
+ void reset()
+ {
+ bufIndex = 0;
+ for (auto &x : buf) x.fill({});
+
+ delayTimeRateLimiter.value = delayTimeSamples;
+ delay.reset();
+ bandpass.reset();
+
+ safetyGain = 0;
+ }
+
+ void noteOn() { safetyGain = Sample(1); }
+
+ Sample process(
+ Sample input,
+ Sample crossGain,
+ Sample pitchMod,
+ Sample timeModAmount,
+ FeedbackMatrix &feedbackMatrix)
+ {
+ bufIndex ^= 1;
+ auto &front = buf[bufIndex];
+ auto &back = buf[bufIndex ^ 1];
+ front.fill({});
+ // feedbackMatrix.process();
+ for (size_t i = 0; i < length; ++i) {
+ feedbackMatrix.matrix[i].process();
+ for (size_t j = 0; j < length; ++j) front[i] += feedbackMatrix.at(i, j) * back[j];
+ }
+
+ // input /= Sample(length);
+ const auto feedbackGain = safetyGain * crossGain;
+ for (size_t i = 0; i < length; ++i) front[i] = input + feedbackGain * front[i];
+
+ bandpassCutoff.process();
+ bandpass.process(front, bandpassCutoff.value, bandpassQ.process(), pitchMod);
+
+ delayTimeRateLimiter.process(delayTimeSamples, front, timeModAmount);
+ delay.process(front, delayTimeRateLimiter.value, pitchMod);
+
+ const auto sum = std::accumulate(front.begin(), front.end(), Sample(0));
+ if (Sample(length) < sum) {
+ safetyGain *= sum <= Sample(100) ? crossDecayGentle : crossDecaySteep;
+ }
+ return sum;
+ }
+};
+
+} // namespace SomeDSP
diff --git a/GenericDrum/source/editor.cpp b/GenericDrum/source/editor.cpp
new file mode 100644
index 00000000..7927dc92
--- /dev/null
+++ b/GenericDrum/source/editor.cpp
@@ -0,0 +1,414 @@
+// (c) 2021-2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#include "editor.hpp"
+#include "../../lib/pcg-cpp/pcg_random.hpp"
+#include "version.hpp"
+
+#include
+#include
+
+constexpr float uiTextSize = 12.0f;
+constexpr float pluginNameTextSize = 14.0f;
+constexpr float margin = 5.0f;
+constexpr float uiMargin = 20.0f;
+constexpr float labelHeight = 20.0f;
+constexpr float knobWidth = 80.0f;
+constexpr float knobX = knobWidth + 2 * margin;
+constexpr float knobY = knobWidth + labelHeight + 2 * margin;
+constexpr float labelY = labelHeight + 2 * margin;
+constexpr float labelWidth = 2 * knobWidth;
+constexpr float groupLabelWidth = 2 * labelWidth + 2 * margin;
+constexpr float splashWidth = int(labelWidth * 3 / 2) + 2 * margin;
+constexpr float splashHeight = int(labelHeight * 3 / 2);
+
+constexpr float barBoxWidth = groupLabelWidth;
+constexpr float barBoxHeight = 5 * labelY - 2 * margin;
+constexpr float smallKnobWidth = labelHeight;
+constexpr float smallKnobX = smallKnobWidth + 2 * margin;
+
+constexpr float tabViewWidth = 2 * groupLabelWidth + 4 * margin + 2 * uiMargin;
+constexpr float tabViewHeight = 20 * labelY - 2 * margin + 2 * uiMargin;
+
+constexpr int_least32_t defaultWidth = int_least32_t(4 * uiMargin + 3 * groupLabelWidth);
+constexpr int_least32_t defaultHeight
+ = int_least32_t(2 * uiMargin + 20 * labelY - 2 * margin);
+
+enum tabIndex { tabBatter, tabSnare };
+
+namespace Steinberg {
+namespace Vst {
+
+using namespace VSTGUI;
+
+Editor::Editor(void *controller) : PlugEditor(controller)
+{
+ param = std::make_unique();
+
+ viewRect = ViewRect{0, 0, int32(defaultWidth), int32(defaultHeight)};
+ setRect(viewRect);
+}
+
+bool Editor::prepareUI()
+{
+ using ID = Synth::ParameterID::ID;
+ using Scales = Synth::Scales;
+ using Style = Uhhyou::Style;
+
+ constexpr auto top0 = uiMargin;
+ constexpr auto top1 = top0 + 1 * labelY;
+ constexpr auto top2 = top0 + 2 * labelY;
+ constexpr auto top3 = top0 + 3 * labelY;
+ constexpr auto top4 = top0 + 4 * labelY;
+ constexpr auto top5 = top0 + 5 * labelY;
+ constexpr auto top6 = top0 + 6 * labelY;
+ constexpr auto top7 = top0 + 7 * labelY;
+ constexpr auto top8 = top0 + 8 * labelY;
+ constexpr auto top9 = top0 + 9 * labelY;
+ constexpr auto top10 = top0 + 10 * labelY;
+ constexpr auto top11 = top0 + 11 * labelY;
+ constexpr auto top12 = top0 + 12 * labelY;
+ constexpr auto top13 = top0 + 13 * labelY;
+ constexpr auto top14 = top0 + 14 * labelY;
+ constexpr auto top15 = top0 + 15 * labelY;
+ constexpr auto top16 = top0 + 16 * labelY;
+ constexpr auto top17 = top0 + 17 * labelY;
+ constexpr auto top18 = top0 + 18 * labelY;
+ constexpr auto top19 = top0 + 19 * labelY;
+ constexpr auto top20 = top0 + 20 * labelY;
+ constexpr auto top21 = top0 + 21 * labelY;
+ constexpr auto top22 = top0 + 22 * labelY;
+ constexpr auto top23 = top0 + 23 * labelY;
+ constexpr auto left0 = uiMargin;
+ constexpr auto left4 = left0 + 1 * groupLabelWidth + 4 * margin;
+ constexpr auto left8 = left0 + 2 * groupLabelWidth + 4 * margin + uiMargin;
+
+ // Mix.
+ constexpr auto mixTop0 = top0;
+ constexpr auto mixTop1 = mixTop0 + 1 * labelY;
+ constexpr auto mixTop2 = mixTop0 + 2 * labelY;
+ constexpr auto mixTop3 = mixTop0 + 3 * labelY;
+ constexpr auto mixLeft0 = left0;
+ constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix");
+
+ addLabel(mixLeft0, mixTop1, labelWidth, labelHeight, uiTextSize, "Output [dB]");
+ addTextKnob(
+ mixLeft1, mixTop1, labelWidth, labelHeight, uiTextSize, ID::outputGain, Scales::gain,
+ true, 5);
+ addToggleButton(
+ mixLeft0, mixTop2, labelWidth, labelHeight, uiTextSize, "Highpass [Hz]",
+ ID::safetyHighpassEnable);
+ addTextKnob(
+ mixLeft1, mixTop2, labelWidth, labelHeight, uiTextSize, ID::safetyHighpassHz,
+ Scales::safetyHighpassHz, false, 5);
+ addCheckbox(
+ mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, "2x Sampling",
+ ID::overSampling);
+
+ // Tuning.
+ constexpr auto tuningTop0 = top0 + 4 * labelY;
+ constexpr auto tuningTop1 = tuningTop0 + 1 * labelY;
+ constexpr auto tuningTop2 = tuningTop0 + 2 * labelY;
+ constexpr auto tuningTop3 = tuningTop0 + 3 * labelY;
+ constexpr auto tuningTop4 = tuningTop0 + 4 * labelY;
+ constexpr auto tuningTop5 = tuningTop0 + 5 * labelY;
+ constexpr auto tuningTop6 = tuningTop0 + 6 * labelY;
+ constexpr auto tuningLeft0 = left0;
+ constexpr auto tuningLeft1 = tuningLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ tuningLeft0, tuningTop0, groupLabelWidth, labelHeight, uiTextSize, "Tuning");
+
+ addLabel(tuningLeft0, tuningTop1, labelWidth, labelHeight, uiTextSize, "Semitone");
+ addTextKnob(
+ tuningLeft1, tuningTop1, labelWidth, labelHeight, uiTextSize, ID::tuningSemitone,
+ Scales::semitone, false, 0, -semitoneOffset);
+ addLabel(tuningLeft0, tuningTop2, labelWidth, labelHeight, uiTextSize, "Cent");
+ addTextKnob(
+ tuningLeft1, tuningTop2, labelWidth, labelHeight, uiTextSize, ID::tuningCent,
+ Scales::cent, false, 5);
+ addLabel(tuningLeft0, tuningTop3, labelWidth, labelHeight, uiTextSize, "Equal Temp.");
+ addTextKnob(
+ tuningLeft1, tuningTop3, labelWidth, labelHeight, uiTextSize, ID::tuningET,
+ Scales::equalTemperament, false, 0, 1);
+ addLabel(
+ tuningLeft0, tuningTop4, labelWidth, labelHeight, uiTextSize,
+ "Pitch Bend Range [st.]");
+ addTextKnob(
+ tuningLeft1, tuningTop4, labelWidth, labelHeight, uiTextSize, ID::pitchBendRange,
+ Scales::pitchBendRange, false, 5);
+ addLabel(
+ tuningLeft0, tuningTop5, labelWidth, labelHeight, uiTextSize, "Slide Time [s]");
+ addTextKnob(
+ tuningLeft1, tuningTop5, labelWidth, labelHeight, uiTextSize, ID::noteSlideTimeSecond,
+ Scales::noteSlideTimeSecond, false, 5);
+ constexpr auto slideAtWidth = int(groupLabelWidth / 3);
+ constexpr auto slideAtLeft1 = tuningLeft0 + 1 * slideAtWidth;
+ constexpr auto slideAtLeft2 = tuningLeft0 + 2 * slideAtWidth;
+ addLabel(tuningLeft0, tuningTop6, slideAtWidth, labelHeight, uiTextSize, "Slide at");
+ addCheckbox(
+ slideAtLeft1, tuningTop6, slideAtWidth, labelHeight, uiTextSize, "Note-on",
+ ID::slideAtNoteOn);
+ addCheckbox(
+ slideAtLeft2, tuningTop6, slideAtWidth, labelHeight, uiTextSize, "Note-off",
+ ID::slideAtNoteOff);
+
+ // Impact.
+ constexpr auto impactTop0 = top0 + 0 * labelY;
+ constexpr auto impactTop1 = impactTop0 + 1 * labelY;
+ constexpr auto impactTop2 = impactTop0 + 2 * labelY;
+ constexpr auto impactTop3 = impactTop0 + 3 * labelY;
+ constexpr auto impactTop4 = impactTop0 + 4 * labelY;
+ constexpr auto impactLeft0 = left4;
+ constexpr auto impactLeft1 = impactLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ impactLeft0, impactTop0, groupLabelWidth, labelHeight, uiTextSize, "Impact");
+
+ addLabel(impactLeft0, impactTop1, labelWidth, labelHeight, uiTextSize, "Seed");
+ auto seedTextKnob = addTextKnob(
+ impactLeft1, impactTop1, labelWidth, labelHeight, uiTextSize, ID::seed, Scales::seed,
+ false, 0);
+ if (seedTextKnob) {
+ seedTextKnob->sensitivity = 2048.0 / double(1 << 24);
+ seedTextKnob->lowSensitivity = 1.0 / double(1 << 24);
+ }
+ addLabel(
+ impactLeft0, impactTop2, labelWidth, labelHeight, uiTextSize, "Noise Decay [s]");
+ addTextKnob(
+ impactLeft1, impactTop2, labelWidth, labelHeight, uiTextSize, ID::noiseDecaySeconds,
+ Scales::noiseDecaySeconds, false, 5);
+ addLabel(
+ impactLeft0, impactTop3, labelWidth, labelHeight, uiTextSize, "Noise Lowpass [Hz]");
+ addTextKnob(
+ impactLeft1, impactTop3, labelWidth, labelHeight, uiTextSize, ID::noiseLowpassHz,
+ Scales::delayTimeHz, false, 5);
+ addLabel(impactLeft0, impactTop4, labelWidth, labelHeight, uiTextSize, "Echo [Hz]");
+ addTextKnob(
+ impactLeft1, impactTop4, labelWidth, labelHeight, uiTextSize,
+ ID::noiseAllpassMaxTimeHz, Scales::delayTimeHz, false, 5);
+
+ // Wire.
+ constexpr auto wireTop0 = top0 + 5 * labelY;
+ constexpr auto wireTop1 = wireTop0 + 1 * labelY;
+ constexpr auto wireTop2 = wireTop0 + 2 * labelY;
+ constexpr auto wireTop3 = wireTop0 + 3 * labelY;
+ constexpr auto wireTop4 = wireTop0 + 4 * labelY;
+ constexpr auto wireTop5 = wireTop0 + 5 * labelY;
+ constexpr auto wireTop6 = wireTop0 + 6 * labelY;
+ constexpr auto wireTop7 = wireTop0 + 7 * labelY;
+ constexpr auto wireLeft0 = left4;
+ constexpr auto wireLeft1 = wireLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(wireLeft0, wireTop0, groupLabelWidth, labelHeight, uiTextSize, "Wire");
+
+ addLabel(wireLeft0, wireTop1, labelWidth, labelHeight, uiTextSize, "Impact-Wire Mix");
+ addTextKnob(
+ wireLeft1, wireTop1, labelWidth, labelHeight, uiTextSize, ID::impactWireMix,
+ Scales::defaultScale, false, 5);
+ addLabel(wireLeft0, wireTop2, labelWidth, labelHeight, uiTextSize, "Membrane-Wire Mix");
+ addTextKnob(
+ wireLeft1, wireTop2, labelWidth, labelHeight, uiTextSize, ID::membraneWireMix,
+ Scales::defaultScale, false, 5);
+ addLabel(wireLeft0, wireTop3, labelWidth, labelHeight, uiTextSize, "Frequency [Hz]");
+ addTextKnob(
+ wireLeft1, wireTop3, labelWidth, labelHeight, uiTextSize, ID::wireFrequencyHz,
+ Scales::wireFrequencyHz, false, 5);
+ addLabel(wireLeft0, wireTop4, labelWidth, labelHeight, uiTextSize, "Decay [s]");
+ addTextKnob(
+ wireLeft1, wireTop4, labelWidth, labelHeight, uiTextSize, ID::wireDecaySeconds,
+ Scales::wireDecaySeconds, false, 5);
+ addLabel(
+ wireLeft0, wireTop5, labelWidth, labelHeight, uiTextSize, "Collision Distance");
+ addTextKnob(
+ wireLeft1, wireTop5, labelWidth, labelHeight, uiTextSize, ID::wireDistance,
+ Scales::collisionDistance, false, 5);
+ addLabel(wireLeft0, wireTop6, labelWidth, labelHeight, uiTextSize, "Ruttle-Squeak Mix");
+ addTextKnob(
+ wireLeft1, wireTop6, labelWidth, labelHeight, uiTextSize, ID::wireCollisionTypeMix,
+ Scales::defaultScale, false, 5);
+ addLabel(
+ wireLeft0, wireTop7, 2 * labelWidth, labelHeight, uiTextSize,
+ "Wire collision status.");
+
+ // Primary Membrane.
+ constexpr auto primaryTop0 = top0 + 13 * labelY;
+ constexpr auto primaryTop1 = primaryTop0 + 1 * labelY;
+ constexpr auto primaryTop2 = primaryTop0 + 2 * labelY;
+ constexpr auto primaryLeft0 = left4;
+ constexpr auto primaryLeft1 = primaryLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ primaryLeft0, primaryTop0, groupLabelWidth, labelHeight, uiTextSize,
+ "Primary Membrane");
+
+ addLabel(
+ primaryLeft0, primaryTop1, labelWidth, labelHeight, uiTextSize,
+ "Cross Feedback Gain [dB]");
+ addTextKnob(
+ primaryLeft1, primaryTop1, labelWidth, labelHeight, uiTextSize, ID::crossFeedbackGain,
+ Scales::crossFeedbackGain, false, 5);
+ addBarBox(
+ primaryLeft0, primaryTop2, barBoxWidth, barBoxHeight, ID::crossFeedbackRatio0,
+ maxFdnSize, Scales::defaultScale, "Cross Feedback Ratio");
+
+ // Pitch Texture.
+ constexpr auto textureTop0 = top0;
+ constexpr auto textureTop1 = textureTop0 + 1 * labelY;
+ constexpr auto textureTop2 = textureTop0 + 2 * labelY;
+ constexpr auto textureTop3 = textureTop0 + 3 * labelY;
+ constexpr auto textureLeft0 = left8;
+ constexpr auto textureLeft1 = textureLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ textureLeft0, textureTop0, groupLabelWidth, labelHeight, uiTextSize, "Pitch Texture");
+
+ addLabel(
+ textureLeft0, textureTop1, labelWidth, labelHeight, uiTextSize, "Delay Time Spread");
+ addTextKnob(
+ textureLeft1, textureTop1, labelWidth, labelHeight, uiTextSize, ID::delayTimeSpread,
+ Scales::defaultScale, false, 5);
+ addLabel(
+ textureLeft0, textureTop2, labelWidth, labelHeight, uiTextSize, "BP Cut Spread");
+ addTextKnob(
+ textureLeft1, textureTop2, labelWidth, labelHeight, uiTextSize, ID::bandpassCutSpread,
+ Scales::defaultScale, false, 5);
+ addLabel(
+ textureLeft0, textureTop3, labelWidth, labelHeight, uiTextSize,
+ "Pitch Random [cent]");
+ addTextKnob(
+ textureLeft1, textureTop3, labelWidth, labelHeight, uiTextSize, ID::pitchRandomCent,
+ Scales::pitchRandomCent, false, 5);
+
+ // Pitch Envelope.
+ constexpr auto envTop0 = top0 + 4 * labelY;
+ constexpr auto envTop1 = envTop0 + 1 * labelY;
+ constexpr auto envTop2 = envTop0 + 2 * labelY;
+ constexpr auto envTop3 = envTop0 + 3 * labelY;
+ constexpr auto envLeft0 = left8;
+ constexpr auto envLeft1 = envLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ envLeft0, envTop0, groupLabelWidth, labelHeight, uiTextSize, "Pitch Envelope");
+
+ addLabel(envLeft0, envTop1, labelWidth, labelHeight, uiTextSize, "Attack [s]");
+ addTextKnob(
+ envLeft1, envTop1, labelWidth, labelHeight, uiTextSize, ID::envelopeAttackSeconds,
+ Scales::envelopeSeconds, false, 5);
+ addLabel(envLeft0, envTop2, labelWidth, labelHeight, uiTextSize, "Decay [s]");
+ addTextKnob(
+ envLeft1, envTop2, labelWidth, labelHeight, uiTextSize, ID::envelopeDecaySeconds,
+ Scales::envelopeSeconds, false, 5);
+ addLabel(envLeft0, envTop3, labelWidth, labelHeight, uiTextSize, "Amount [oct]");
+ addTextKnob(
+ envLeft1, envTop3, labelWidth, labelHeight, uiTextSize, ID::envelopeModAmount,
+ Scales::envelopeModAmount, false, 5);
+
+ // Pitch Main.
+ constexpr auto mainTop0 = top0 + 8 * labelY;
+ constexpr auto mainTop1 = mainTop0 + 1 * labelY;
+ constexpr auto mainTop2 = mainTop0 + 2 * labelY;
+ constexpr auto mainTop3 = mainTop0 + 3 * labelY;
+ constexpr auto mainTop4 = mainTop0 + 4 * labelY;
+ constexpr auto mainTop5 = mainTop0 + 5 * labelY;
+ constexpr auto mainLeft0 = left8;
+ constexpr auto mainLeft1 = mainLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ mainLeft0, mainTop0, groupLabelWidth, labelHeight, uiTextSize, "Pitch Main");
+
+ addLabel(mainLeft0, mainTop1, labelWidth, labelHeight, uiTextSize, "Pitch Type");
+ std::vector pitchTypeItems{
+ "Harmonic",
+ "Harmonic+12",
+ "Harmonic*5",
+ "Harmonic Cycle(1, 5)",
+ "Harmonic Odd",
+ "Semitone (1, 2, 7, 9)",
+ "Circular Membrane Mode",
+ "Prime Number",
+ "Octave",
+ };
+ addOptionMenu(
+ mainLeft1, mainTop1, labelWidth, labelHeight, uiTextSize, ID::pitchType,
+ {"Harmonic", "Harmonic+12", "Harmonic*5", "Harmonic Cycle(1, 5)", "Harmonic Odd",
+ "Semitone (1, 2, 7, 9)", "Circular Membrane Mode", "Prime Number", "Octave"});
+ addLabel(mainLeft0, mainTop2, labelWidth, labelHeight, uiTextSize, "Delay [Hz]");
+ addTextKnob(
+ mainLeft1, mainTop2, labelWidth, labelHeight, uiTextSize, ID::delayTimeHz,
+ Scales::delayTimeHz, false, 5);
+ addLabel(
+ mainLeft0, mainTop3, labelWidth, labelHeight, uiTextSize,
+ "Delay Moddulation [sample]");
+ addTextKnob(
+ mainLeft1, mainTop3, labelWidth, labelHeight, uiTextSize, ID::delayTimeModAmount,
+ Scales::delayTimeModAmount, false, 5);
+ addLabel(mainLeft0, mainTop4, labelWidth, labelHeight, uiTextSize, "BP Cut [oct]");
+ addTextKnob(
+ mainLeft1, mainTop4, labelWidth, labelHeight, uiTextSize, ID::bandpassCutRatio,
+ Scales::bandpassCutRatio, false, 5);
+ addLabel(mainLeft0, mainTop5, labelWidth, labelHeight, uiTextSize, "BP Q");
+ addTextKnob(
+ mainLeft1, mainTop5, labelWidth, labelHeight, uiTextSize, ID::bandpassQ,
+ Scales::bandpassQ, false, 5);
+
+ // Secondary Membrane.
+ constexpr auto secondaryTop0 = top0 + 14 * labelY;
+ constexpr auto secondaryTop1 = secondaryTop0 + 1 * labelY;
+ constexpr auto secondaryTop2 = secondaryTop0 + 2 * labelY;
+ constexpr auto secondaryTop3 = secondaryTop0 + 3 * labelY;
+ constexpr auto secondaryTop4 = secondaryTop0 + 4 * labelY;
+ constexpr auto secondaryTop5 = secondaryTop0 + 5 * labelY;
+ constexpr auto secondaryLeft0 = left8;
+ constexpr auto secondaryLeft1 = secondaryLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ secondaryLeft0, secondaryTop0, groupLabelWidth, labelHeight, uiTextSize,
+ "Secondary Membrane");
+
+ addLabel(secondaryLeft0, secondaryTop1, labelWidth, labelHeight, uiTextSize, "Mix");
+ addTextKnob(
+ secondaryLeft1, secondaryTop1, labelWidth, labelHeight, uiTextSize,
+ ID::secondaryFdnMix, Scales::defaultScale, false, 5);
+ addLabel(
+ secondaryLeft0, secondaryTop2, labelWidth, labelHeight, uiTextSize,
+ "Pitch Offset [oct]");
+ addTextKnob(
+ secondaryLeft1, secondaryTop2, labelWidth, labelHeight, uiTextSize,
+ ID::secondaryPitchOffset, Scales::bandpassCutRatio, false, 5);
+ addLabel(
+ secondaryLeft0, secondaryTop3, labelWidth, labelHeight, uiTextSize, "Q Offset [oct]");
+ addTextKnob(
+ secondaryLeft1, secondaryTop3, labelWidth, labelHeight, uiTextSize,
+ ID::secondaryQOffset, Scales::bandpassCutRatio, false, 5);
+ addLabel(
+ secondaryLeft0, secondaryTop4, labelWidth, labelHeight, uiTextSize,
+ "Collision Distance");
+ addTextKnob(
+ secondaryLeft1, secondaryTop4, labelWidth, labelHeight, uiTextSize,
+ ID::secondaryDistance, Scales::collisionDistance, false, 5);
+ addLabel(
+ secondaryLeft0, secondaryTop5, 2 * labelWidth, labelHeight, uiTextSize,
+ "Membrane collision status.");
+
+ // Plugin name.
+ constexpr auto splashMargin = uiMargin;
+ constexpr auto splashTop = top0 + 13 * labelY + int(labelHeight / 4) + 2 * margin;
+ constexpr auto splashLeft = left0 + int(labelWidth / 4);
+ addSplashScreen(
+ splashLeft, splashTop, splashWidth, splashHeight, splashMargin, splashMargin,
+ defaultWidth - 2 * splashMargin, defaultHeight - 2 * splashMargin, pluginNameTextSize,
+ "GenericDrum", true);
+
+ return true;
+}
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/GenericDrum/source/editor.hpp b/GenericDrum/source/editor.hpp
new file mode 100644
index 00000000..9f67f219
--- /dev/null
+++ b/GenericDrum/source/editor.hpp
@@ -0,0 +1,43 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "../../common/gui/plugeditor.hpp"
+#include "parameter.hpp"
+
+#include
+#include
+#include
+
+namespace Steinberg {
+namespace Vst {
+
+using namespace VSTGUI;
+
+class Editor : public PlugEditor {
+public:
+ Editor(void *controller);
+
+ DELEGATE_REFCOUNT(VSTGUIEditor);
+
+private:
+ bool prepareUI() override;
+};
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/GenericDrum/source/fuid.hpp b/GenericDrum/source/fuid.hpp
new file mode 100644
index 00000000..4e513ffc
--- /dev/null
+++ b/GenericDrum/source/fuid.hpp
@@ -0,0 +1,30 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include "pluginterfaces/base/funknown.h"
+
+namespace Steinberg {
+namespace Synth {
+
+// https://www.guidgenerator.com/
+static const FUID ProcessorUID(0x97B971DA, 0x2A0E4E0B, 0x9B3F1278, 0xDC9BFB60);
+static const FUID ControllerUID(0xC8F061CA, 0xA91B450F, 0x96A7A24A, 0x17D09B6B);
+
+} // namespace Synth
+} // namespace Steinberg
diff --git a/GenericDrum/source/gui/splashdraw.cpp b/GenericDrum/source/gui/splashdraw.cpp
new file mode 100644
index 00000000..cc36541e
--- /dev/null
+++ b/GenericDrum/source/gui/splashdraw.cpp
@@ -0,0 +1,97 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#include "../../../common/gui/splash.hpp"
+#include "../version.hpp"
+
+namespace Steinberg {
+namespace Vst {
+
+using namespace VSTGUI;
+
+void CreditView::draw(CDrawContext *pContext)
+{
+ pContext->setDrawMode(CDrawMode(CDrawModeFlags::kAntiAliasing));
+ CDrawContext::Transform t(
+ *pContext, CGraphicsTransform().translate(getViewSize().getTopLeft()));
+
+ const auto width = getWidth();
+ const auto height = getHeight();
+ const double borderWidth = 2.0;
+ const double halfBorderWidth = borderWidth / 2.0;
+
+ // Background.
+ pContext->setLineWidth(borderWidth);
+ pContext->setFillColor(pal.background());
+ pContext->drawRect(CRect(0.0, 0.0, width, height), kDrawFilled);
+
+ // Border.
+ pContext->setFrameColor(isMouseEntered ? pal.highlightMain() : pal.border());
+ pContext->drawRect(
+ CRect(
+ halfBorderWidth, halfBorderWidth, width - halfBorderWidth,
+ height - halfBorderWidth),
+ kDrawStroked);
+
+ // Text.
+ pContext->setFont(fontIdTitle);
+ pContext->setFontColor(pal.foreground());
+ pContext->drawString("GenericDrum " VERSION_STR, CPoint(20.0, 40.0));
+
+ pContext->setFont(fontIdText);
+ pContext->setFontColor(pal.foreground());
+ pContext->drawString("© 2023 Takamitsu Endo (ryukau@gmail.com)", CPoint(20.0f, 60.0f));
+
+ std::string leftText = R"(This plugin is beta version.
+Breaking changes may be introduced.
+
+- Do not use for production.
+- Do not save your project with this plugin.
+- Always render or record the result to audio file.
+
+Click to dismiss this message.)";
+
+ std::string midText = R"(- Number & Knob -
+Shift + Left Drag|Fine Adjustment
+Ctrl + Left Click|Reset to Default
+Middle Click|Flip Min/Mid/Max
+Shift + Middle Click|Take Floor
+
+GenericDrum can output very loud signal.
+Recommend to use with limiter.
+
+Breath.Gain and Pulse.Gain are fed into non-linear
+component. Those two affect the character of sound.
+To change loudness without affecting the character,
+use Mix.Output.
+
+Have a nice day!)";
+
+ std::string rightText = R"()";
+
+ const float top0 = 100.0f;
+ const float lineHeight = 20.0f;
+ const float blockWidth = 115.0f;
+ drawTextBlock(pContext, 20.0f, top0, lineHeight, blockWidth, leftText);
+ drawTextBlock(pContext, 320.0f, top0, lineHeight, blockWidth, midText);
+ drawTextBlock(pContext, 620.0f, top0, lineHeight, blockWidth, rightText);
+
+ setDirty(false);
+}
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/GenericDrum/source/parameter.cpp b/GenericDrum/source/parameter.cpp
new file mode 100644
index 00000000..dc852551
--- /dev/null
+++ b/GenericDrum/source/parameter.cpp
@@ -0,0 +1,67 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#include "parameter.hpp"
+
+#include
+#include
+
+namespace Steinberg {
+namespace Synth {
+
+using namespace SomeDSP;
+
+template inline T ampToDB(T amp) { return T(20) * std::log10(amp); }
+
+constexpr auto eps = std::numeric_limits::epsilon();
+
+UIntScale Scales::boolScale(1);
+LinearScale Scales::defaultScale(0.0, 1.0);
+LinearScale Scales::bipolarScale(-1.0, 1.0);
+UIntScale Scales::seed(1 << 23);
+
+DecibelScale Scales::gain(-100.0, 60.0, true);
+DecibelScale Scales::safetyHighpassHz(ampToDB(0.1), ampToDB(100.0), false);
+
+UIntScale Scales::semitone(semitoneOffset + 48);
+LinearScale Scales::cent(-100.0, 100.0);
+UIntScale Scales::equalTemperament(119);
+LinearScale Scales::pitchBendRange(0.0, 120.0);
+DecibelScale Scales::noteSlideTimeSecond(-100.0, 40.0, true);
+
+DecibelScale Scales::noiseDecaySeconds(-40, ampToDB(0.5), false);
+
+DecibelScale Scales::wireFrequencyHz(0, ampToDB(1000), false);
+DecibelScale Scales::wireDecaySeconds(-40, 40, false);
+
+DecibelScale Scales::crossFeedbackGain(-12, 0, false);
+DecibelScale Scales::feedbackDecaySeconds(-40, 20, false);
+
+LinearScale Scales::pitchRandomCent(0, 1200);
+DecibelScale Scales::envelopeSeconds(-60, 40, false);
+DecibelScale Scales::envelopeModAmount(-20, 20, true);
+
+UIntScale Scales::pitchType(8);
+DecibelScale Scales::delayTimeHz(ampToDB(2), ampToDB(10000), false);
+DecibelScale Scales::delayTimeModAmount(-20, 100, true);
+LinearScale Scales::bandpassCutRatio(-8, 8);
+DecibelScale Scales::bandpassQ(-40, 40, false);
+
+DecibelScale Scales::collisionDistance(-80, 40, true);
+
+} // namespace Synth
+} // namespace Steinberg
diff --git a/GenericDrum/source/parameter.hpp b/GenericDrum/source/parameter.hpp
new file mode 100644
index 00000000..af30060d
--- /dev/null
+++ b/GenericDrum/source/parameter.hpp
@@ -0,0 +1,306 @@
+// (c) 2021-2023 Takamitsu Endo
+//
+// This file is part of GenericDrum.
+//
+// GenericDrum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// GenericDrum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with GenericDrum. If not, see .
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "../../common/dsp/constants.hpp"
+#include "../../common/parameterInterface.hpp"
+
+#ifdef TEST_DSP
+ #include "../../test/value.hpp"
+#else
+ #include "../../common/value.hpp"
+#endif
+
+constexpr int octaveOffset = 8;
+constexpr int semitoneOffset = 96;
+constexpr size_t maxFdnSize = 5;
+
+namespace Steinberg {
+namespace Synth {
+
+namespace ParameterID {
+enum ID {
+ bypass,
+
+ outputGain,
+ safetyHighpassEnable,
+ safetyHighpassHz,
+ overSampling,
+
+ tuningSemitone,
+ tuningCent,
+ tuningET,
+ pitchBend,
+ pitchBendRange,
+ noteSlideTimeSecond,
+ slideAtNoteOn,
+ slideAtNoteOff,
+
+ seed,
+ noiseDecaySeconds,
+ noiseLowpassHz,
+ noiseAllpassMaxTimeHz,
+
+ impactWireMix,
+ membraneWireMix,
+ wireFrequencyHz,
+ wireDecaySeconds,
+ wireDistance,
+ wireCollisionTypeMix,
+
+ crossFeedbackGain,
+ crossFeedbackRatio0,
+
+ delayTimeSpread = crossFeedbackRatio0 + maxFdnSize,
+ bandpassCutSpread,
+ pitchRandomCent,
+
+ envelopeAttackSeconds,
+ envelopeDecaySeconds,
+ envelopeModAmount,
+
+ pitchType,
+ delayTimeHz,
+ delayTimeModAmount,
+ bandpassCutRatio,
+ bandpassQ,
+
+ secondaryFdnMix,
+ secondaryPitchOffset,
+ secondaryQOffset,
+ secondaryDistance,
+
+ ID_ENUM_LENGTH,
+ // ID_ENUM_GUI_START = ID_ENUM_LENGTH,
+};
+} // namespace ParameterID
+
+struct Scales {
+ static SomeDSP::UIntScale boolScale;
+ static SomeDSP::LinearScale defaultScale;
+ static SomeDSP::LinearScale bipolarScale;
+ static SomeDSP::UIntScale seed;
+
+ static SomeDSP::DecibelScale gain;
+ static SomeDSP::DecibelScale safetyHighpassHz;
+
+ static SomeDSP::UIntScale semitone;
+ static SomeDSP::LinearScale cent;
+ static SomeDSP::UIntScale equalTemperament;
+ static SomeDSP::LinearScale pitchBendRange;
+ static SomeDSP::DecibelScale noteSlideTimeSecond;
+
+ static SomeDSP::DecibelScale noiseDecaySeconds;
+
+ static SomeDSP::DecibelScale wireFrequencyHz;
+ static SomeDSP::DecibelScale wireDecaySeconds;
+
+ static SomeDSP::DecibelScale crossFeedbackGain;
+ static SomeDSP::DecibelScale feedbackDecaySeconds;
+
+ static SomeDSP::LinearScale pitchRandomCent;
+ static SomeDSP::DecibelScale envelopeSeconds;
+ static SomeDSP::DecibelScale envelopeModAmount;
+
+ static SomeDSP::UIntScale pitchType;
+ static SomeDSP::DecibelScale delayTimeHz;
+ static SomeDSP::DecibelScale delayTimeModAmount;
+ static SomeDSP::LinearScale bandpassCutRatio;
+ static SomeDSP::DecibelScale bandpassQ;
+
+ static SomeDSP::DecibelScale collisionDistance;
+};
+
+struct GlobalParameter : public ParameterInterface {
+ std::vector> value;
+
+ GlobalParameter()
+ {
+ value.resize(ParameterID::ID_ENUM_LENGTH);
+
+ using Info = Vst::ParameterInfo;
+ using ID = ParameterID::ID;
+ using LinearValue = DoubleValue>;
+ using DecibelValue = DoubleValue>;
+ using NegativeDecibelValue = DoubleValue>;
+
+ value[ID::bypass] = std::make_unique(
+ 0, Scales::boolScale, "bypass", Info::kCanAutomate | Info::kIsBypass);
+
+ value[ID::outputGain] = std::make_unique(
+ Scales::gain.invmap(1.0), Scales::gain, "outputGain", Info::kCanAutomate);
+ value[ID::safetyHighpassEnable] = std::make_unique(
+ 1, Scales::boolScale, "safetyHighpassEnable", Info::kCanAutomate);
+ value[ID::safetyHighpassHz] = std::make_unique(
+ Scales::safetyHighpassHz.invmap(4.0), Scales::safetyHighpassHz, "safetyHighpassHz",
+ Info::kCanAutomate);
+ value[ID::overSampling] = std::make_unique(
+ 1, Scales::boolScale, "overSampling", Info::kCanAutomate);
+
+ value[ID::tuningSemitone] = std::make_unique(
+ semitoneOffset, Scales::semitone, "tuningSemitone", Info::kCanAutomate);
+ value[ID::tuningCent] = std::make_unique(
+ Scales::cent.invmap(0.0), Scales::cent, "tuningCent", Info::kCanAutomate);
+ value[ID::tuningET] = std::make_unique(
+ 11, Scales::equalTemperament, "tuningET", Info::kCanAutomate);
+ value[ID::pitchBend] = std::make_unique(
+ 0.5, Scales::bipolarScale, "pitchBend", Info::kCanAutomate);
+ value[ID::pitchBendRange] = std::make_unique(
+ Scales::pitchBendRange.invmap(2.0), Scales::pitchBendRange, "pitchBendRange",
+ Info::kCanAutomate);
+ value[ID::noteSlideTimeSecond] = std::make_unique(
+ Scales::noteSlideTimeSecond.invmap(0.0001), Scales::noteSlideTimeSecond,
+ "noteSlideTimeSecond", Info::kCanAutomate);
+ value[ID::slideAtNoteOn] = std::make_unique(
+ 0, Scales::boolScale, "slideAtNoteOn", Info::kCanAutomate);
+ value[ID::slideAtNoteOff] = std::make_unique(
+ 0, Scales::boolScale, "slideAtNoteOff", Info::kCanAutomate);
+
+ value[ID::seed]
+ = std::make_unique(0, Scales::seed, "seed", Info::kCanAutomate);
+ value[ID::noiseDecaySeconds] = std::make_unique(
+ Scales::noiseDecaySeconds.invmap(0.08), Scales::noiseDecaySeconds,
+ "noiseDecaySeconds", Info::kCanAutomate);
+ value[ID::noiseLowpassHz] = std::make_unique(
+ Scales::delayTimeHz.invmap(50.0), Scales::delayTimeHz, "noiseLowpassHz",
+ Info::kCanAutomate);
+ value[ID::noiseAllpassMaxTimeHz] = std::make_unique(
+ Scales::delayTimeHz.invmap(3000.0), Scales::delayTimeHz, "noiseAllpassMaxTimeHz",
+ Info::kCanAutomate);
+
+ value[ID::impactWireMix] = std::make_unique(
+ Scales::defaultScale.invmap(0.9), Scales::defaultScale, "impactWireMix",
+ Info::kCanAutomate);
+ value[ID::membraneWireMix] = std::make_unique(
+ Scales::defaultScale.invmap(0), Scales::defaultScale, "membraneWireMix",
+ Info::kCanAutomate);
+ value[ID::wireFrequencyHz] = std::make_unique(
+ Scales::wireFrequencyHz.invmap(100.0), Scales::wireFrequencyHz, "wireFrequencyHz",
+ Info::kCanAutomate);
+ value[ID::wireDecaySeconds] = std::make_unique(
+ Scales::wireDecaySeconds.invmap(2.0), Scales::wireDecaySeconds, "wireDecaySeconds",
+ Info::kCanAutomate);
+ value[ID::wireDistance] = std::make_unique(
+ Scales::collisionDistance.invmap(0.15), Scales::collisionDistance, "wireDistance",
+ Info::kCanAutomate);
+ value[ID::wireCollisionTypeMix] = std::make_unique(
+ Scales::defaultScale.invmap(0.5), Scales::defaultScale, "wireCollisionTypeMix",
+ Info::kCanAutomate);
+
+ value[ID::crossFeedbackGain] = std::make_unique(
+ Scales::crossFeedbackGain.invmapDB(-1), Scales::crossFeedbackGain,
+ "crossFeedbackGain", Info::kCanAutomate);
+ for (size_t idx = 0; idx < maxFdnSize; ++idx) {
+ auto indexStr = std::to_string(idx);
+
+ value[ID::crossFeedbackRatio0 + idx] = std::make_unique(
+ Scales::defaultScale.invmap(1.0), Scales::defaultScale,
+ ("crossFeedbackRatio" + indexStr).c_str(), Info::kCanAutomate);
+ }
+
+ value[ID::delayTimeSpread] = std::make_unique(
+ Scales::defaultScale.invmap(0.1), Scales::defaultScale, "delayTimeSpread",
+ Info::kCanAutomate);
+ value[ID::bandpassCutSpread] = std::make_unique(
+ Scales::defaultScale.invmap(0.5), Scales::defaultScale, "bandpassCutSpread",
+ Info::kCanAutomate);
+ value[ID::pitchRandomCent] = std::make_unique(
+ Scales::pitchRandomCent.invmap(21.5), Scales::pitchRandomCent, "pitchRandomCent",
+ Info::kCanAutomate);
+
+ value[ID::envelopeAttackSeconds] = std::make_unique(
+ Scales::envelopeSeconds.invmap(0.01), Scales::envelopeSeconds,
+ "envelopeAttackSeconds", Info::kCanAutomate);
+ value[ID::envelopeDecaySeconds] = std::make_unique(
+ Scales::envelopeSeconds.invmap(0.01), Scales::envelopeSeconds,
+ "envelopeDecaySeconds", Info::kCanAutomate);
+ value[ID::envelopeModAmount] = std::make_unique(
+ 0, Scales::envelopeModAmount, "envelopeModAmount", Info::kCanAutomate);
+
+ value[ID::pitchType] = std::make_unique(
+ 7, Scales::pitchType, "pitchType", Info::kCanAutomate);
+ value[ID::delayTimeHz] = std::make_unique(
+ Scales::delayTimeHz.invmap(110.0), Scales::delayTimeHz, "delayTimeHz",
+ Info::kCanAutomate);
+ value[ID::delayTimeModAmount] = std::make_unique(
+ Scales::delayTimeModAmount.invmap(1150.0), Scales::delayTimeModAmount,
+ "delayTimeModAmount", Info::kCanAutomate);
+ value[ID::bandpassCutRatio] = std::make_unique(
+ Scales::bandpassCutRatio.invmap(0.7), Scales::bandpassCutRatio, "bandpassCutRatio",
+ Info::kCanAutomate);
+ value[ID::bandpassQ] = std::make_unique(
+ Scales::bandpassQ.invmap(0.1), Scales::bandpassQ, "bandpassQ", Info::kCanAutomate);
+
+ value[ID::secondaryFdnMix] = std::make_unique