From b5ede97904a9ec51820821e38b078606f783d7c8 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Sun, 1 Oct 2023 12:35:25 +0900 Subject: [PATCH] Add GenericDrum --- CMakeLists.txt | 3 +- GenericDrum/CMakeLists.txt | 18 + GenericDrum/resource/Info.plist | 28 ++ GenericDrum/resource/plug.rc | 44 +++ GenericDrum/source/controller.hpp | 85 +++++ GenericDrum/source/dsp/dspcore.cpp | 469 +++++++++++++++++++++++ GenericDrum/source/dsp/dspcore.hpp | 156 ++++++++ GenericDrum/source/dsp/envelope.hpp | 136 +++++++ GenericDrum/source/dsp/filter.hpp | 529 ++++++++++++++++++++++++++ GenericDrum/source/editor.cpp | 414 ++++++++++++++++++++ GenericDrum/source/editor.hpp | 43 +++ GenericDrum/source/fuid.hpp | 30 ++ GenericDrum/source/gui/splashdraw.cpp | 97 +++++ GenericDrum/source/parameter.cpp | 67 ++++ GenericDrum/source/parameter.hpp | 306 +++++++++++++++ GenericDrum/source/plugfactory.cpp | 73 ++++ GenericDrum/source/plugprocessor.cpp | 177 +++++++++ GenericDrum/source/plugprocessor.hpp | 68 ++++ GenericDrum/source/version.hpp | 56 +++ GenericDrum/test/testdsp.cpp | 34 ++ common/dsp/smoother.hpp | 7 + 21 files changed, 2839 insertions(+), 1 deletion(-) create mode 100644 GenericDrum/CMakeLists.txt create mode 100644 GenericDrum/resource/Info.plist create mode 100644 GenericDrum/resource/plug.rc create mode 100644 GenericDrum/source/controller.hpp create mode 100644 GenericDrum/source/dsp/dspcore.cpp create mode 100644 GenericDrum/source/dsp/dspcore.hpp create mode 100644 GenericDrum/source/dsp/envelope.hpp create mode 100644 GenericDrum/source/dsp/filter.hpp create mode 100644 GenericDrum/source/editor.cpp create mode 100644 GenericDrum/source/editor.hpp create mode 100644 GenericDrum/source/fuid.hpp create mode 100644 GenericDrum/source/gui/splashdraw.cpp create mode 100644 GenericDrum/source/parameter.cpp create mode 100644 GenericDrum/source/parameter.hpp create mode 100644 GenericDrum/source/plugfactory.cpp create mode 100644 GenericDrum/source/plugprocessor.cpp create mode 100644 GenericDrum/source/plugprocessor.hpp create mode 100644 GenericDrum/source/version.hpp create mode 100644 GenericDrum/test/testdsp.cpp 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( + Scales::defaultScale.invmap(0.25), Scales::defaultScale, "secondaryFdnMix", + Info::kCanAutomate); + value[ID::secondaryPitchOffset] = std::make_unique( + Scales::bandpassCutRatio.invmap(0.65), Scales::bandpassCutRatio, + "secondaryPitchOffset", Info::kCanAutomate); + value[ID::secondaryQOffset] = std::make_unique( + Scales::bandpassCutRatio.invmap(-2), Scales::bandpassCutRatio, "secondaryQOffset", + Info::kCanAutomate); + value[ID::secondaryDistance] = std::make_unique( + Scales::collisionDistance.invmap(0.0008), Scales::collisionDistance, + "secondaryDistance", Info::kCanAutomate); + + for (size_t id = 0; id < value.size(); ++id) value[id]->setId(Vst::ParamID(id)); + } + +#ifdef TEST_DSP + // Not used in DSP test. + double getDefaultNormalized(int32_t) { return 0.0; } + +#else + tresult setState(IBStream *stream) + { + IBStreamer streamer(stream, kLittleEndian); + for (auto &val : value) + if (val->setState(streamer)) return kResultFalse; + return kResultOk; + } + + tresult getState(IBStream *stream) + { + IBStreamer streamer(stream, kLittleEndian); + for (auto &val : value) + if (val->getState(streamer)) return kResultFalse; + return kResultOk; + } + + tresult addParameter(Vst::ParameterContainer ¶meters) + { + for (auto &val : value) + if (val->addParameter(parameters)) return kResultFalse; + return kResultOk; + } + + double getDefaultNormalized(int32_t tag) override + { + if (size_t(abs(tag)) >= value.size()) return 0.0; + return value[tag]->getDefaultNormalized(); + } +#endif +}; + +} // namespace Synth +} // namespace Steinberg diff --git a/GenericDrum/source/plugfactory.cpp b/GenericDrum/source/plugfactory.cpp new file mode 100644 index 00000000..6d52eb3e --- /dev/null +++ b/GenericDrum/source/plugfactory.cpp @@ -0,0 +1,73 @@ +// Original by: +// (c) 2018, Steinberg Media Technologies GmbH, All Rights Reserved +// +// Modified by: +// (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 "pluginterfaces/vst/ivstaudioprocessor.h" +#include "public.sdk/source/main/pluginfactory.h" + +#include "controller.hpp" +#include "editor.hpp" +#include "fuid.hpp" +#include "plugprocessor.hpp" +#include "version.hpp" + +// Subcategory for this Plug-in (see PlugType in ivstaudioprocessor.h) +#define stringSubCategory Steinberg::Vst::PlugType::kInstrumentSynth + +BEGIN_FACTORY_DEF(stringCompanyName, stringCompanyWeb, stringCompanyEmail) + +DEF_CLASS2( + INLINE_UID_FROM_FUID(Steinberg::Synth::ProcessorUID), + PClassInfo::kManyInstances, // cardinality + kVstAudioEffectClass, // the component category (do not changed this) + stringPluginName, // here the Plug-in name (to be changed) + Vst::kDistributable, + stringSubCategory, // Subcategory for this Plug-in (to be changed) + FULL_VERSION_STR, // Plug-in version (to be changed) + kVstVersionString, // SDK Version (do not changed this, use always this define) + Steinberg::Synth::PlugProcessor::createInstance) + +using Controller = Steinberg::Synth::PlugController; + +DEF_CLASS2( + INLINE_UID_FROM_FUID(Steinberg::Synth::ControllerUID), + PClassInfo::kManyInstances, // cardinality + kVstComponentControllerClass, // the Controller category (do not changed this) + stringPluginName + "Controller", // controller name (could be the same than component name) + 0, // not used here + "", // not used here + FULL_VERSION_STR, // Plug-in version (to be changed) + kVstVersionString, // SDK Version (do not changed this, use always this define) + Controller::createInstance) + +END_FACTORY + +//------------------------------------------------------------------------ +// Module init/exit +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// called after library was loaded +inline bool InitModule() { return true; } + +//------------------------------------------------------------------------ +// called after library is unloaded +inline bool DeinitModule() { return true; } diff --git a/GenericDrum/source/plugprocessor.cpp b/GenericDrum/source/plugprocessor.cpp new file mode 100644 index 00000000..1f54eba2 --- /dev/null +++ b/GenericDrum/source/plugprocessor.cpp @@ -0,0 +1,177 @@ +// Modified by: +// (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 "plugprocessor.hpp" +#include "fuid.hpp" + +#include "base/source/fstreamer.h" +#include "pluginterfaces/base/ibstream.h" +#include "pluginterfaces/vst/ivstaudioprocessor.h" +#include "pluginterfaces/vst/ivstevents.h" +#include "pluginterfaces/vst/ivstparameterchanges.h" + +#ifdef USE_VECTORCLASS + #include "../../lib/vcl/vectorclass.h" +#endif + +#include + +namespace Steinberg { +namespace Synth { + +PlugProcessor::PlugProcessor() { setControllerClass(ControllerUID); } + +tresult PLUGIN_API PlugProcessor::initialize(FUnknown *context) +{ + tresult result = AudioEffect::initialize(context); + if (result != kResultTrue) return result; + + addAudioOutput(STR16("StereoOutput"), Vst::SpeakerArr::kStereo); + addEventInput(STR16("EventInput"), 1); + + return result; +} + +tresult PLUGIN_API PlugProcessor::setBusArrangements( + Vst::SpeakerArrangement *inputs, + int32 numIns, + Vst::SpeakerArrangement *outputs, + int32 numOuts) +{ + if (numIns == 1 && numOuts == 1 && inputs[0] == outputs[0]) { + return AudioEffect::setBusArrangements(inputs, numIns, outputs, numOuts); + } + return kResultFalse; +} + +uint32 PLUGIN_API PlugProcessor::getProcessContextRequirements() +{ + using Rq = Vst::IProcessContextRequirements; + + return Rq::kNeedProjectTimeMusic & Rq::kNeedTempo & Rq::kNeedTransportState + & Rq::kNeedTimeSignature; +} + +tresult PLUGIN_API PlugProcessor::setupProcessing(Vst::ProcessSetup &setup) +{ + dsp.setup(processSetup.sampleRate); + return AudioEffect::setupProcessing(setup); +} + +tresult PLUGIN_API PlugProcessor::setActive(TBool state) +{ + if (state) { + dsp.setup(processSetup.sampleRate); + } else { + dsp.reset(); + lastState = 0; + } + return AudioEffect::setActive(state); +} + +tresult PLUGIN_API PlugProcessor::process(Vst::ProcessData &data) +{ + using ID = ParameterID::ID; + + // Read inputs parameter changes. + if (data.inputParameterChanges) { + int32 parameterCount = data.inputParameterChanges->getParameterCount(); + for (int32 index = 0; index < parameterCount; index++) { + auto queue = data.inputParameterChanges->getParameterData(index); + if (!queue) continue; + Vst::ParamValue value; + int32 sampleOffset; + if (queue->getPoint(queue->getPointCount() - 1, sampleOffset, value) != kResultTrue) + continue; + size_t id = queue->getParameterId(); + if (id < dsp.param.value.size()) dsp.param.value[id]->setFromNormalized(value); + } + } + + if (data.processContext != nullptr) { + uint64_t state = data.processContext->state; + if (state & Vst::ProcessContext::kTempoValid) { + dsp.tempo = data.processContext->tempo; + } + if (state & Vst::ProcessContext::kProjectTimeMusicValid) { + dsp.beatsElapsed = data.processContext->projectTimeMusic; + } + if (state & Vst::ProcessContext::kTimeSigValid) { + dsp.timeSigLower = data.processContext->timeSigDenominator; + dsp.timeSigUpper = data.processContext->timeSigNumerator; + } + if (!dsp.isPlaying && (state & Vst::ProcessContext::kPlaying) != 0) { + dsp.startup(); + } + dsp.isPlaying = state & Vst::ProcessContext::kPlaying; + } + + dsp.setParameters(); + + if (data.numOutputs == 0) return kResultOk; + if (data.numSamples <= 0) return kResultOk; + if (data.outputs[0].numChannels < 2) return kResultOk; + if (data.symbolicSampleSize == Vst::kSample64) return kResultOk; + + if (data.inputEvents != nullptr) handleEvent(data); + + float *out0 = data.outputs[0].channelBuffers32[0]; + float *out1 = data.outputs[0].channelBuffers32[1]; + dsp.process((size_t)data.numSamples, out0, out1); + + return kResultOk; +} + +void PlugProcessor::handleEvent(Vst::ProcessData &data) +{ + for (int32 index = 0; index < data.inputEvents->getEventCount(); ++index) { + Vst::Event event; + if (data.inputEvents->getEvent(index, event) != kResultOk) continue; + switch (event.type) { + case Vst::Event::kNoteOnEvent: { + dsp.pushMidiNote( + true, event.sampleOffset, + event.noteOn.noteId == -1 ? event.noteOn.pitch : event.noteOn.noteId, + event.noteOn.pitch, event.noteOn.tuning, event.noteOn.velocity); + } break; + + case Vst::Event::kNoteOffEvent: { + dsp.pushMidiNote( + false, event.sampleOffset, + event.noteOff.noteId == -1 ? event.noteOff.pitch : event.noteOff.noteId, 0, 0, + 0); + } break; + + // Add other event type here. + } + } +} + +tresult PLUGIN_API PlugProcessor::setState(IBStream *state) +{ + if (!state) return kResultFalse; + return dsp.param.setState(state); +} + +tresult PLUGIN_API PlugProcessor::getState(IBStream *state) +{ + return dsp.param.getState(state); +} + +} // namespace Synth +} // namespace Steinberg diff --git a/GenericDrum/source/plugprocessor.hpp b/GenericDrum/source/plugprocessor.hpp new file mode 100644 index 00000000..5ee3c9e7 --- /dev/null +++ b/GenericDrum/source/plugprocessor.hpp @@ -0,0 +1,68 @@ +// Original by: +// (c) 2018, Steinberg Media Technologies GmbH, All Rights Reserved +// +// Modified by: +// (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 "public.sdk/source/vst/vstaudioeffect.h" + +#include "dsp/dspcore.hpp" + +namespace Steinberg { +namespace Synth { + +class PlugProcessor : public Vst::AudioEffect { +public: + PlugProcessor(); + + tresult PLUGIN_API initialize(FUnknown *context) SMTG_OVERRIDE; + tresult PLUGIN_API setBusArrangements( + Vst::SpeakerArrangement *inputs, + int32 numIns, + Vst::SpeakerArrangement *outputs, + int32 numOuts) SMTG_OVERRIDE; + uint32 PLUGIN_API getProcessContextRequirements() SMTG_OVERRIDE; + + tresult PLUGIN_API setupProcessing(Vst::ProcessSetup &setup) SMTG_OVERRIDE; + tresult PLUGIN_API setActive(TBool state) SMTG_OVERRIDE; + tresult PLUGIN_API process(Vst::ProcessData &data) SMTG_OVERRIDE; + + tresult PLUGIN_API setState(IBStream *state) SMTG_OVERRIDE; + tresult PLUGIN_API getState(IBStream *state) SMTG_OVERRIDE; + + static FUnknown *createInstance(void *) + { + return (Vst::IAudioProcessor *)new PlugProcessor(); + } + +protected: + void handleEvent(Vst::ProcessData &data); + + inline int32 toDiscrete(Vst::ParamValue normalized, int32 stepCount) + { + return int32(std::min(stepCount, normalized * (stepCount + 1.0))); + } + + uint64_t lastState = 0; + DSPCore dsp; +}; + +} // namespace Synth +} // namespace Steinberg diff --git a/GenericDrum/source/version.hpp b/GenericDrum/source/version.hpp new file mode 100644 index 00000000..839e86cc --- /dev/null +++ b/GenericDrum/source/version.hpp @@ -0,0 +1,56 @@ +// Original by: +// (c) 2018, Steinberg Media Technologies GmbH, All Rights Reserved +// +// Modified by: +// (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/fplatform.h" + +#define MAJOR_VERSION_STR "0" +#define MAJOR_VERSION_INT 0 + +#define SUB_VERSION_STR "0" +#define SUB_VERSION_INT 0 + +#define RELEASE_NUMBER_STR "0" +#define RELEASE_NUMBER_INT 0 + +#define BUILD_NUMBER_STR "0" +#define BUILD_NUMBER_INT 0 + +#define FULL_VERSION_STR \ + MAJOR_VERSION_STR "." SUB_VERSION_STR "." RELEASE_NUMBER_STR "." BUILD_NUMBER_STR + +#define VERSION_STR MAJOR_VERSION_STR "." SUB_VERSION_STR "." RELEASE_NUMBER_STR + +#define stringPluginName "GenericDrum" + +#define stringOriginalFilename "GenericDrum.vst3" +#if SMTG_PLATFORM_64 + #define stringFileDescription stringPluginName " VST3-SDK (64Bit)" +#else + #define stringFileDescription stringPluginName " VST3-SDK" +#endif +#define stringCompanyName "Uhhyou\0" +#define stringCompanyWeb "" +#define stringCompanyEmail "ryukau@gmail.com" + +#define stringLegalCopyright "Copyright 2023 Takamitsu Endo" +#define stringLegalTrademarks "VST is a trademark of Steinberg Media Technologies GmbH" diff --git a/GenericDrum/test/testdsp.cpp b/GenericDrum/test/testdsp.cpp new file mode 100644 index 00000000..91969e0e --- /dev/null +++ b/GenericDrum/test/testdsp.cpp @@ -0,0 +1,34 @@ +// (c) 2023 Takamitsu Endo +// +// This file is part of Uhhyou Plugins. +// +// Uhhyou Plugins 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. +// +// Uhhyou Plugins 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 Uhhyou Plugins. If not, see . + +#define SET_PARAMETERS dsp->setParameters(); + +#include "../../test/synthtester.hpp" +#include "../source/dsp/dspcore.hpp" + +// CMake provides this macro, but just in case. +#ifndef UHHYOU_PLUGIN_NAME + #define UHHYOU_PLUGIN_NAME "GenericDrum" +#endif + +#define OUT_DIR_PATH "snd/" UHHYOU_PLUGIN_NAME + +int main() +{ + SynthTester tester(UHHYOU_PLUGIN_NAME, OUT_DIR_PATH, 1); + return tester.isFinished ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/common/dsp/smoother.hpp b/common/dsp/smoother.hpp index 3bcf82d8..57544d5b 100644 --- a/common/dsp/smoother.hpp +++ b/common/dsp/smoother.hpp @@ -147,6 +147,10 @@ template class ExpSmoother { } void push(Sample newTarget) { target = newTarget; } + + // Intended to be used after `push`. + void catchUp() { value = target; } + Sample process() { return value += SmootherCommon::kp * (target - value); } }; @@ -182,6 +186,9 @@ template class ParallelExpSmoother { inline void pushAt(size_t index, Sample newTarget) { target[index] = newTarget; } + void catchUp() { value = target; } + void catchUpAt(size_t index) { value[index] = target[index]; } + void process() { for (size_t i = 0; i < length; ++i) {