diff --git a/LoopCymbal/CMakeLists.txt b/LoopCymbal/CMakeLists.txt
new file mode 100644
index 00000000..a3619970
--- /dev/null
+++ b/LoopCymbal/CMakeLists.txt
@@ -0,0 +1,17 @@
+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/LoopCymbal/resource/Info.plist b/LoopCymbal/resource/Info.plist
new file mode 100644
index 00000000..70c80ec0
--- /dev/null
+++ b/LoopCymbal/resource/Info.plist
@@ -0,0 +1,28 @@
+
+
+
+
+ NSHumanReadableCopyright
+ 2018 Steinberg Media Technologies
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ LoopCymbal
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ com.steinberg.vst3.LoopCymbal
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ CFBundleShortVersionString
+ 1.0
+ CSResourcesFileMapped
+
+
+
diff --git a/LoopCymbal/resource/plug.rc b/LoopCymbal/resource/plug.rc
new file mode 100644
index 00000000..128ff068
--- /dev/null
+++ b/LoopCymbal/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/LoopCymbal/source/controller.hpp b/LoopCymbal/source/controller.hpp
new file mode 100644
index 00000000..04dd102e
--- /dev/null
+++ b/LoopCymbal/source/controller.hpp
@@ -0,0 +1,85 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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/LoopCymbal/source/dsp/delay.hpp b/LoopCymbal/source/dsp/delay.hpp
new file mode 100644
index 00000000..44bc0bc3
--- /dev/null
+++ b/LoopCymbal/source/dsp/delay.hpp
@@ -0,0 +1,172 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace SomeDSP {
+
+template class ExpDecay {
+private:
+ Sample value = Sample(0);
+ Sample alpha = Sample(0);
+
+public:
+ void setTime(Sample decayTimeInSamples)
+ {
+ alpha = std::pow(
+ Sample(std::numeric_limits::epsilon()), Sample(1) / decayTimeInSamples);
+ }
+
+ void reset() { value = Sample(0); }
+ void noteOn() { value = Sample(1); }
+ Sample process() { return value *= alpha; }
+};
+
+template inline T lagrange3Interp(T y0, T y1, T y2, T y3, T t)
+{
+ auto u = T(1) + t;
+ auto d0 = y0 - y1;
+ auto d1 = d0 - (y1 - y2);
+ auto d2 = d1 - ((y1 - y2) - (y2 - y3));
+ return y0 - u * (d0 + (T(1) - u) / T(2) * (d1 + (T(2) - u) / T(3) * d2));
+}
+
+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(4), size_t(maxTimeSamples) + 4));
+ reset();
+ }
+
+ void reset() { std::fill(buf.begin(), buf.end(), Sample(0)); }
+
+ Sample process(Sample input, Sample timeInSamples)
+ {
+ const int size = int(buf.size());
+ const int clamped
+ = std::clamp(timeInSamples - Sample(1), Sample(1), Sample(size - 4));
+ const int timeInt = int(clamped);
+ const Sample rFraction = clamped - Sample(timeInt);
+
+ // Write to buffer.
+ if (++wptr >= size) wptr = 0;
+ buf[wptr] = input;
+
+ // Read from buffer.
+ auto rptr0 = wptr - timeInt;
+ auto rptr1 = rptr0 - 1;
+ auto rptr2 = rptr0 - 2;
+ auto rptr3 = rptr0 - 3;
+ if (rptr0 < 0) rptr0 += size;
+ if (rptr1 < 0) rptr1 += size;
+ if (rptr2 < 0) rptr2 += size;
+ if (rptr3 < 0) rptr3 += size;
+ return lagrange3Interp(buf[rptr0], buf[rptr1], buf[rptr2], buf[rptr3], rFraction);
+ }
+};
+
+// Unused. `SerialAllpass` became unstable when used.
+template class NyquistLowpass {
+private:
+ static constexpr Sample g = Sample(15915.494288237813); // tan(0.49998 * pi).
+ static constexpr Sample k = Sample(0.7071067811865476); // 2 / sqrt(2).
+
+ Sample ic1eq = 0;
+ Sample ic2eq = 0;
+
+public:
+ void reset()
+ {
+ ic1eq = 0;
+ ic2eq = 0;
+ }
+
+ Sample process(Sample input)
+ {
+ const auto v1 = (ic1eq + g * (input - ic2eq)) / (1 + g * (g + k));
+ const auto v2 = ic2eq + g * v1;
+ ic1eq = Sample(2) * v1 - ic1eq;
+ ic2eq = Sample(2) * v2 - ic2eq;
+ return v2;
+ }
+};
+
+template class EmaHighShelf {
+private:
+ Sample value = 0;
+
+public:
+ void reset() { value = 0; }
+
+ Sample process(Sample input, Sample kp, Sample shelvingGain)
+ {
+ value += kp * (input - value);
+ return std::lerp(value, input, shelvingGain);
+ }
+};
+
+template class SerialAllpass {
+private:
+ std::array buffer{};
+ std::array, nAllpass> delay;
+ std::array, nAllpass> lowpass;
+
+public:
+ static constexpr size_t size = nAllpass;
+ std::array timeInSamples{};
+
+ void setup(Sample maxTimeSamples)
+ {
+ for (auto &x : delay) x.setup(maxTimeSamples);
+ }
+
+ void reset()
+ {
+ buffer.fill({});
+ for (auto &x : delay) x.reset();
+ for (auto &x : lowpass) x.reset();
+ }
+
+ Sample process(
+ Sample input,
+ Sample highShelfCut,
+ Sample highShelfGain,
+ Sample gain,
+ Sample timeModAmount)
+ {
+ for (size_t idx = 0; idx < nAllpass; ++idx) {
+ auto x0 = lowpass[idx].process(input, highShelfCut, highShelfGain);
+ x0 -= gain * buffer[idx];
+ input = buffer[idx] + gain * x0;
+ buffer[idx]
+ = delay[idx].process(x0, timeInSamples[idx] - timeModAmount * std::abs(x0));
+ }
+ return input;
+ }
+};
+
+} // namespace SomeDSP
diff --git a/LoopCymbal/source/dsp/dspcore.cpp b/LoopCymbal/source/dsp/dspcore.cpp
new file mode 100644
index 00000000..e41b7898
--- /dev/null
+++ b/LoopCymbal/source/dsp/dspcore.cpp
@@ -0,0 +1,332 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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;
+}
+
+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 * 2 * Scales::delayTimeSecond.getMax();
+ for (auto &x : serialAllpass) x.setup(maxDelayTimeSamples);
+
+ reset();
+ startup();
+}
+
+#define ASSIGN_PARAMETER(METHOD) \
+ using ID = ParameterID::ID; \
+ const auto &pv = param.value; \
+ \
+ useExternalInput = pv[ID::useExternalInput]->getInt(); \
+ \
+ 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); \
+ \
+ externalInputGain.METHOD(pv[ID::externalInputGain]->getDouble()); \
+ delayTimeModAmount.METHOD( \
+ pv[ID::delayTimeModAmount]->getDouble() * upRate / double(48000)); \
+ allpassFeed.METHOD(pv[ID::allpassFeed]->getDouble()); \
+ hihatDistance.METHOD(pv[ID::hihatDistance]->getDouble()); \
+ stereoBalance.METHOD(pv[ID::stereoBalance]->getDouble()); \
+ stereoMerge.METHOD(pv[ID::stereoMerge]->getDouble() / double(2)); \
+ \
+ auto gain = pv[ID::outputGain]->getDouble(); \
+ outputGain.METHOD(gain); \
+ \
+ envelope.setTime(pv[ID::noiseDecaySeconds]->getDouble() * upRate); \
+ \
+ paramRng.seed(pv[ID::seed]->getInt()); \
+ const auto delayTimeBase = pv[ID::delayTimeBaseSecond]->getDouble() * upRate; \
+ const auto delayTimeRandom = pv[ID::delayTimeRandomSecond]->getDouble() * upRate; \
+ std::uniform_real_distribution delayTimeDist{ \
+ double(0), double(delayTimeRandom)}; \
+ for (auto &ap : serialAllpass) { \
+ for (size_t idx = 0; idx < nAllpass; ++idx) { \
+ ap.timeInSamples[idx] = delayTimeBase / double(idx + 1) + delayTimeDist(paramRng); \
+ } \
+ }
+
+void DSPCore::updateUpRate()
+{
+ upRate = sampleRate * fold[overSampling];
+ SmootherCommon::setSampleRate(upRate);
+}
+
+void DSPCore::reset()
+{
+ noteNumber = 57.0;
+ velocity = 0;
+
+ overSampling = param.value[ParameterID::ID::overSampling]->getInt();
+ updateUpRate();
+
+ ASSIGN_PARAMETER(reset);
+
+ startup();
+
+ impulse = 0;
+ noiseGain = 0;
+ noiseDecay = 0;
+ envelope.reset();
+ feedbackBuffer.fill(double(0));
+ for (auto &x : serialAllpass) x.reset();
+
+ for (auto &x : halfbandInput) x.fill({});
+ for (auto &x : halfbandIir) x.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)
+{
+ auto diff = p0 - p1 + distance;
+ if (diff >= 0) return;
+
+ const auto r0 = std::abs(v0);
+ const auto r1 = std::abs(v1);
+ if (r0 + r1 > std::numeric_limits::epsilon()) diff /= r0 + r1;
+ p0 = -diff * r1;
+ p1 = diff * r0;
+}
+
+std::array DSPCore::processFrame(const std::array &externalInput)
+{
+ const auto extGain = externalInputGain.process();
+ const auto timeModAmt = delayTimeModAmount.process();
+ const auto apGain = allpassFeed.process();
+ const auto distance = hihatDistance.process();
+ const auto balance = stereoBalance.process();
+ const auto merge = stereoMerge.process();
+ const auto outGain = outputGain.process();
+
+ std::uniform_real_distribution dist{double(-1), double(1)};
+ const auto noiseEnv = envelope.process();
+ std::array excitation{
+ -apGain * feedbackBuffer[0], -apGain * feedbackBuffer[1]};
+ if (impulse != 0) {
+ excitation[0] += impulse;
+ excitation[1] += impulse;
+ impulse = 0;
+ } else {
+ excitation[0] += noiseEnv * dist(noiseRng);
+ excitation[1] += noiseEnv * dist(noiseRng);
+ }
+
+ if (useExternalInput) {
+ excitation[0] += externalInput[0] * extGain;
+ excitation[1] += externalInput[1] * extGain;
+ }
+
+ auto cymbal0 = feedbackBuffer[0];
+ auto cymbal1 = feedbackBuffer[1];
+ feedbackBuffer[0]
+ = serialAllpass[0].process(excitation[0], double(1), double(1), apGain, timeModAmt);
+ feedbackBuffer[1]
+ = serialAllpass[1].process(excitation[1], double(1), double(1), apGain, timeModAmt);
+
+ // TODO
+ solveCollision(
+ feedbackBuffer[0], feedbackBuffer[1], feedbackBuffer[0] - cymbal0,
+ feedbackBuffer[1] - cymbal1, distance);
+
+ constexpr auto eps = std::numeric_limits::epsilon();
+ if (balance < -eps) {
+ cymbal0 *= double(1) + balance;
+ } else if (balance > eps) {
+ cymbal1 *= double(1) - balance;
+ }
+ return {
+ outGain * std::lerp(cymbal0, cymbal1, merge),
+ outGain * std::lerp(cymbal1, cymbal0, merge),
+ };
+}
+
+void DSPCore::process(
+ const size_t length, const float *in0, const float *in1, float *out0, float *out1)
+{
+ ScopedNoDenormals scopedDenormals;
+
+ using ID = ParameterID::ID;
+ const auto &pv = param.value;
+
+ SmootherCommon::setBufferSize(double(length));
+ SmootherCommon::setSampleRate(upRate);
+
+ std::array prevExtIn = halfbandInput[0];
+ std::array frame{};
+ for (size_t i = 0; i < length; ++i) {
+ processMidiNote(i);
+
+ const double extIn0 = in0 == nullptr ? 0 : in0[i];
+ const double extIn1 = in1 == nullptr ? 0 : in1[i];
+
+ if (overSampling) {
+ frame = processFrame({
+ double(0.5) * (prevExtIn[0] + extIn0),
+ double(0.5) * (prevExtIn[1] + extIn1),
+ });
+ halfbandInput[0][0] = frame[0];
+ halfbandInput[1][0] = frame[1];
+
+ frame = processFrame({extIn0, extIn1});
+ halfbandInput[0][1] = frame[0];
+ halfbandInput[1][1] = frame[1];
+
+ frame[0] = halfbandIir[0].process(halfbandInput[0]);
+ frame[1] = halfbandIir[1].process(halfbandInput[1]);
+ out0[i] = float(frame[0]);
+ out1[i] = float(frame[1]);
+ } else {
+ frame = processFrame({extIn0, extIn1});
+ out0[i] = float(frame[0]);
+ out1[i] = float(frame[1]);
+ }
+
+ prevExtIn = {extIn0, extIn1};
+ }
+}
+
+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());
+ interpPitch.push(notePitch);
+
+ velocity = velocityMap.map(info.velocity);
+
+ if (pv[ID::resetSeedAtNoteOn]->getInt()) noiseRng.seed(pv[ID::seed]->getInt());
+
+ impulse = double(1);
+ noiseGain = velocity;
+ noiseDecay = std::pow(
+ double(1e-3), double(1) / (upRate * pv[ID::noiseDecaySeconds]->getDouble()));
+
+ envelope.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()) {
+ 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 notePitchAmount = pv[ID::notePitchAmount]->getDouble();
+ return std::exp2(notePitchAmount * (note + semitone + cent) / double(12));
+}
diff --git a/LoopCymbal/source/dsp/dspcore.hpp b/LoopCymbal/source/dsp/dspcore.hpp
new file mode 100644
index 00000000..eb6f6ec7
--- /dev/null
+++ b/LoopCymbal/source/dsp/dspcore.hpp
@@ -0,0 +1,140 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#pragma once
+
+#include "../../../common/dsp/constants.hpp"
+#include "../../../common/dsp/multirate.hpp"
+#include "../../../common/dsp/smoother.hpp"
+#include "../parameter.hpp"
+#include "delay.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, const float *in0, const float *in1, 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:
+ void updateUpRate();
+ double calcNotePitch(double note);
+ std::array processFrame(const std::array &externalInput);
+
+ 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 externalInputGain;
+ ExpSmoother delayTimeModAmount;
+ ExpSmoother allpassFeed;
+ ExpSmoother hihatDistance;
+ ExpSmoother stereoBalance;
+ ExpSmoother stereoMerge;
+ ExpSmoother outputGain;
+
+ static constexpr size_t nAllpass = 18;
+
+ bool useExternalInput = false;
+
+ std::minstd_rand noiseRng{0};
+ std::minstd_rand paramRng{0};
+ double impulse = 0;
+ double noiseGain = 0;
+ double noiseDecay = 0;
+ ExpDecay envelope;
+ std::array feedbackBuffer{};
+ std::array, 2> serialAllpass;
+
+ std::array, 2> halfbandInput{};
+ std::array>, 2> halfbandIir;
+};
diff --git a/LoopCymbal/source/editor.cpp b/LoopCymbal/source/editor.cpp
new file mode 100644
index 00000000..a276b70c
--- /dev/null
+++ b/LoopCymbal/source/editor.cpp
@@ -0,0 +1,260 @@
+// (c) 2021-2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#include "editor.hpp"
+#include "../../lib/pcg-cpp/pcg_random.hpp"
+#include "gui/randomizebutton.hpp"
+#include "version.hpp"
+
+#include
+#include
+#include
+#include
+
+constexpr float uiTextSize = 12.0f;
+constexpr float pluginNameTextSize = 16.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(2 * labelHeight + 2 * margin);
+
+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);
+
+constexpr const char *wireDidntCollidedText = "Wire didn't collide.";
+constexpr const char *membraneDidntCollidedText = "Membrane didn't collide.";
+
+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);
+}
+
+ParamValue Editor::getPlainValue(ParamID id)
+{
+ auto normalized = controller->getParamNormalized(id);
+ return controller->normalizedParamToPlain(id, normalized);
+}
+
+void Editor::valueChanged(CControl *pControl)
+{
+ using ID = Synth::ParameterID::ID;
+
+ ParamID id = pControl->getTag();
+ ParamValue value = pControl->getValueNormalized();
+ controller->setParamNormalized(id, value);
+ controller->performEdit(id, value);
+}
+
+void Editor::updateUI(ParamID id, ParamValue normalized)
+{
+ using ID = Synth::ParameterID::ID;
+
+ PlugEditor::updateUI(id, normalized);
+}
+
+bool Editor::prepareUI()
+{
+ using ID = Synth::ParameterID::ID;
+ using Scales = Synth::Scales;
+ using Style = Uhhyou::Style;
+
+ constexpr auto top0 = uiMargin;
+ 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 mixTop4 = mixTop0 + 4 * labelY;
+ constexpr auto mixTop5 = mixTop0 + 5 * labelY;
+ constexpr auto mixTop6 = mixTop0 + 6 * labelY;
+ constexpr auto mixTop7 = mixTop0 + 7 * labelY;
+ constexpr auto mixTop8 = mixTop0 + 8 * labelY;
+ constexpr auto mixTop9 = mixTop0 + 9 * labelY;
+ constexpr auto mixTop10 = mixTop0 + 10 * labelY;
+ constexpr auto mixTop11 = mixTop0 + 11 * labelY;
+ constexpr auto mixLeft0 = left0;
+ constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix & Options");
+
+ addLabel(mixLeft0, mixTop1, labelWidth, labelHeight, uiTextSize, "Output [dB]");
+ addTextKnob(
+ mixLeft1, mixTop1, labelWidth, labelHeight, uiTextSize, ID::outputGain, Scales::gain,
+ true, 5);
+ addCheckbox(
+ mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "2x Sampling",
+ ID::overSampling);
+ addCheckbox(
+ mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Reset Seed at Note-on",
+ ID::resetSeedAtNoteOn);
+
+ addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Stereo Balance");
+ addTextKnob(
+ mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::stereoBalance,
+ Scales::bipolarScale, false, 5);
+ addLabel(mixLeft0, mixTop7, labelWidth, labelHeight, uiTextSize, "Stereo Merge");
+ addTextKnob(
+ mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::stereoMerge,
+ Scales::defaultScale, false, 5);
+
+ addToggleButton(
+ mixLeft0, mixTop8, groupLabelWidth, labelHeight, uiTextSize, "External Input",
+ ID::useExternalInput);
+ addLabel(mixLeft0, mixTop9, labelWidth, labelHeight, uiTextSize, "External Gain [dB]");
+ addTextKnob(
+ mixLeft1, mixTop9, labelWidth, labelHeight, uiTextSize, ID::externalInputGain,
+ Scales::gain, true, 5);
+
+ // Tuning.
+ constexpr auto tuningTop0 = top0 + 12 * 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 tuningLeft0 = left0;
+ constexpr auto tuningLeft1 = tuningLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ tuningLeft0, tuningTop0, groupLabelWidth, labelHeight, uiTextSize, "Tuning");
+
+ addLabel(tuningLeft0, tuningTop1, labelWidth, labelHeight, uiTextSize, "Note -> Pitch");
+ addTextKnob(
+ tuningLeft1, tuningTop1, labelWidth, labelHeight, uiTextSize, ID::notePitchAmount,
+ Scales::bipolarScale, false, 5);
+ addLabel(tuningLeft0, tuningTop2, labelWidth, labelHeight, uiTextSize, "Semitone");
+ addTextKnob(
+ tuningLeft1, tuningTop2, labelWidth, labelHeight, uiTextSize, ID::tuningSemitone,
+ Scales::semitone, false, 0, -semitoneOffset);
+ addLabel(tuningLeft0, tuningTop3, labelWidth, labelHeight, uiTextSize, "Cent");
+ addTextKnob(
+ tuningLeft1, tuningTop3, labelWidth, labelHeight, uiTextSize, ID::tuningCent,
+ Scales::cent, false, 5);
+ 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);
+
+ // Cymbal.
+ 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 impactTop5 = impactTop0 + 5 * labelY;
+ constexpr auto impactTop6 = impactTop0 + 6 * labelY;
+ constexpr auto impactTop7 = impactTop0 + 7 * labelY;
+ constexpr auto impactLeft0 = left4;
+ constexpr auto impactLeft1 = impactLeft0 + labelWidth + 2 * margin;
+ addGroupLabel(
+ impactLeft0, impactTop0, groupLabelWidth, labelHeight, uiTextSize, "Cymbal");
+
+ 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, "Delay Base [s]");
+ addTextKnob(
+ impactLeft1, impactTop3, labelWidth, labelHeight, uiTextSize, ID::delayTimeBaseSecond,
+ Scales::delayTimeSecond, false, 5);
+ addLabel(
+ impactLeft0, impactTop4, labelWidth, labelHeight, uiTextSize, "Delay Random [s]");
+ addTextKnob(
+ impactLeft1, impactTop4, labelWidth, labelHeight, uiTextSize,
+ ID::delayTimeRandomSecond, Scales::delayTimeSecond, false, 5);
+ addLabel(
+ impactLeft0, impactTop5, labelWidth, labelHeight, uiTextSize, "Modulation [sample]");
+ addTextKnob(
+ impactLeft1, impactTop5, labelWidth, labelHeight, uiTextSize, ID::delayTimeModAmount,
+ Scales::delayTimeModAmount, false, 5);
+ addLabel(impactLeft0, impactTop6, labelWidth, labelHeight, uiTextSize, "Feed");
+ addTextKnob(
+ impactLeft1, impactTop6, labelWidth, labelHeight, uiTextSize, ID::allpassFeed,
+ Scales::bipolarScale, false, 5);
+ addLabel(impactLeft0, impactTop7, labelWidth, labelHeight, uiTextSize, "Distance");
+ addTextKnob(
+ impactLeft1, impactTop7, labelWidth, labelHeight, uiTextSize, ID::hihatDistance,
+ Scales::hihatDistance, false, 5);
+
+ // Randomize button.
+ const auto randomButtonTop = top0 + 18 * labelY;
+ const auto randomButtonLeft = left0 + labelWidth + 2 * margin;
+ auto panicButton = new RandomizeButton(
+ CRect(
+ randomButtonLeft, randomButtonTop, randomButtonLeft + labelWidth,
+ randomButtonTop + splashHeight),
+ this, 0, "Random", getFont(pluginNameTextSize), palette, this);
+ frame->addView(panicButton);
+
+ // Plugin name.
+ constexpr auto splashMargin = uiMargin;
+ constexpr auto splashTop = top0 + 18 * labelY;
+ constexpr auto splashLeft = left0;
+ addSplashScreen(
+ splashLeft, splashTop, labelWidth, splashHeight, splashMargin, splashMargin,
+ defaultWidth - 2 * splashMargin, defaultHeight - 2 * splashMargin, pluginNameTextSize,
+ "LoopCymbal", false);
+
+ return true;
+}
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/LoopCymbal/source/editor.hpp b/LoopCymbal/source/editor.hpp
new file mode 100644
index 00000000..7cc700e6
--- /dev/null
+++ b/LoopCymbal/source/editor.hpp
@@ -0,0 +1,47 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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);
+
+ virtual void valueChanged(CControl *pControl) override;
+ void updateUI(Vst::ParamID id, ParamValue normalized) override;
+
+ DELEGATE_REFCOUNT(VSTGUIEditor);
+
+private:
+ ParamValue getPlainValue(ParamID id);
+ bool prepareUI() override;
+};
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/LoopCymbal/source/fuid.hpp b/LoopCymbal/source/fuid.hpp
new file mode 100644
index 00000000..7b44263c
--- /dev/null
+++ b/LoopCymbal/source/fuid.hpp
@@ -0,0 +1,30 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#pragma once
+
+#include "pluginterfaces/base/funknown.h"
+
+namespace Steinberg {
+namespace Synth {
+
+// https://www.guidgenerator.com/
+static const FUID ProcessorUID(0x58A4DB52, 0xD9B74A12, 0x85AF5C8D, 0xAB9F9423);
+static const FUID ControllerUID(0x59D483D2, 0xC82144BD, 0x9010DC40, 0x226D4CAA);
+
+} // namespace Synth
+} // namespace Steinberg
diff --git a/LoopCymbal/source/gui/randomizebutton.hpp b/LoopCymbal/source/gui/randomizebutton.hpp
new file mode 100644
index 00000000..89db3dfb
--- /dev/null
+++ b/LoopCymbal/source/gui/randomizebutton.hpp
@@ -0,0 +1,173 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#pragma once
+
+#include "public.sdk/source/vst/vsteditcontroller.h"
+#include "vstgui/vstgui.h"
+
+#include "../../../common/gui/plugeditor.hpp"
+#include "../../../common/gui/style.hpp"
+#include "../parameter.hpp"
+
+#include
+#include
+#include
+
+namespace VSTGUI {
+
+class RandomizeButton : public CControl {
+public:
+ std::string label;
+
+ RandomizeButton(
+ const CRect &size,
+ IControlListener *listener,
+ int32_t tag,
+ std::string label,
+ const SharedPointer &fontId,
+ Uhhyou::Palette &palette,
+ Steinberg::Vst::PlugEditor *editor)
+ : CControl(size, listener, tag)
+ , label(label)
+ , fontId(fontId)
+ , pal(palette)
+ , editor(editor)
+ {
+ if (editor) editor->remember();
+ }
+
+ ~RandomizeButton()
+ {
+ if (editor) editor->forget();
+ }
+
+ void draw(CDrawContext *pContext) override
+ {
+ pContext->setDrawMode(CDrawMode(CDrawModeFlags::kAntiAliasing));
+ CDrawContext::Transform t(
+ *pContext, CGraphicsTransform().translate(getViewSize().getTopLeft()));
+
+ // Border.
+ const double borderW = isMouseEntered ? 2 * borderWidth : borderWidth;
+ const double halfBorderWidth = int(borderW / 2.0);
+ pContext->setFillColor(isPressed ? pal.highlightButton() : pal.boxBackground());
+ pContext->setFrameColor(
+ isMouseEntered && !isPressed ? pal.highlightButton() : pal.border());
+ pContext->setLineWidth(borderW);
+ pContext->drawRect(
+ CRect(
+ halfBorderWidth, halfBorderWidth, getWidth() - halfBorderWidth,
+ getHeight() - halfBorderWidth),
+ kDrawFilledAndStroked);
+
+ // Text
+ pContext->setFont(fontId);
+ pContext->setFontColor(pal.foreground());
+ pContext->drawString(
+ label.c_str(), CRect(0, 0, getWidth(), getHeight()), kCenterText);
+ }
+
+ void onMouseEnterEvent(MouseEnterEvent &event) override
+ {
+ isMouseEntered = true;
+ invalid();
+ event.consumed = true;
+ }
+
+ void onMouseExitEvent(MouseExitEvent &event) override
+ {
+ if (value == 1.0f) {
+ value = 0.0f;
+ }
+ isPressed = false;
+ isMouseEntered = false;
+ invalid();
+ event.consumed = true;
+ }
+
+ void onMouseDownEvent(MouseDownEvent &event) override
+ {
+ using ID = Steinberg::Synth::ParameterID::ID;
+
+ if (!event.buttonState.isLeft()) return;
+ isPressed = true;
+ value = 1.0f;
+
+ if (editor) {
+ using Rng = std::mt19937_64;
+
+ std::random_device source;
+ std::random_device::result_type
+ random_data[(Rng::state_size - 1) / sizeof(source()) + 1];
+ std::generate(std::begin(random_data), std::end(random_data), std::ref(source));
+ std::seed_seq seeds(std::begin(random_data), std::end(random_data));
+ Rng rng(seeds);
+
+ std::uniform_real_distribution uniform{0.0, 1.0};
+ setParam(ID::seed, uniform(rng));
+ }
+
+ invalid();
+ event.consumed = true;
+ }
+
+ void onMouseUpEvent(MouseUpEvent &event) override
+ {
+ if (isPressed) {
+ isPressed = false;
+ value = 0.0f;
+ invalid();
+ }
+ event.consumed = true;
+ }
+
+ void onMouseCancelEvent(MouseCancelEvent &event) override
+ {
+ if (isPressed) {
+ isPressed = false;
+ value = 0;
+ invalid();
+ }
+ isMouseEntered = false;
+ event.consumed = true;
+ }
+
+ void setBorderWidth(CCoord width) { borderWidth = width < 0 ? 0 : width; }
+
+ CLASS_METHODS(RandomizeButton, CControl);
+
+private:
+ // Before calling check if `editor` is not nullptr.
+ inline void setParam(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue value)
+ {
+ editor->valueChanged(id, value);
+ editor->updateUI(id, value);
+ }
+
+ Steinberg::Vst::PlugEditor *editor = nullptr;
+
+ SharedPointer fontId;
+ Uhhyou::Palette &pal;
+
+ CCoord borderWidth = 1.0;
+
+ bool isPressed = false;
+ bool isMouseEntered = false;
+};
+
+} // namespace VSTGUI
diff --git a/LoopCymbal/source/gui/splashdraw.cpp b/LoopCymbal/source/gui/splashdraw.cpp
new file mode 100644
index 00000000..073be9bb
--- /dev/null
+++ b/LoopCymbal/source/gui/splashdraw.cpp
@@ -0,0 +1,126 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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("LoopCymbal " 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 textColumn0 = R"(- Number Sliders -
+Shift + Left Drag|Fine Adjustment
+Ctrl + Left Click|Reset to Default
+Middle Click|Flip Min/Mid/Max
+Shift + Middle Click|Take Floor
+
+- BarBox -
+Ctrl + Left Drag|Reset to Default
+Middle Drag|Draw Line
+Shift + D|Toggle Min/Mid/Max
+I|Invert Value
+P|Permute
+R|Randomize
+S|Sort Decending Order
+T|Random Walk
+Shift + T|Random Walk to 0
+Z|Undo
+Shift + Z|Redo
+, (Comma)|Rotate Back
+. (Period)|Rotate Forward
+1-4|Decrease 1n-4n
+5-9|Hold 2n-5n
+
+Refer to the manual for a full list of shortcuts.)";
+
+ std::string textColumn1 = R"(LoopCymbal can output very loud signal.
+Recommend to use with limiter.
+
+There are 3 places to cause oscillation or blow up:
+
+- Wire-Membrane collision.
+- Membrane-Membrane collision.
+- Membranes.
+
+For wire-membrane collision, a solution is to turn
+on Prevent Blow Up. Note that it also changes the
+sound quite a bit.
+
+On collisions, try raising Collision Distance to
+prevent blow up.
+
+On membranes, high pitch and high Q may cause blow
+up. Watch out for following parameters:
+
+- Note -> Pitch
+- Cross Feedback Gain
+- Cross Feedback Ratio
+- Delay
+- BP Q
+
+Pitch envelope may cause pop noise when at least
+one of Attack or Decay is less than 0.01.
+
+If Note -> Pitch is not 0, and Slide Time is too
+short, it may cause pop noise.)";
+
+ const float top0 = 100.0f;
+ const float lineHeight = 20.0f;
+ const float blockWidth = 115.0f;
+ drawTextBlock(pContext, 20.0f, top0, lineHeight, blockWidth, textColumn0);
+ drawTextBlock(pContext, 320.0f, top0, lineHeight, blockWidth, textColumn1);
+ drawTextBlock(pContext, 620.0f, top0, lineHeight, blockWidth, "Have a nice day!");
+
+ setDirty(false);
+}
+
+} // namespace Vst
+} // namespace Steinberg
diff --git a/LoopCymbal/source/parameter.cpp b/LoopCymbal/source/parameter.cpp
new file mode 100644
index 00000000..47e1cff1
--- /dev/null
+++ b/LoopCymbal/source/parameter.cpp
@@ -0,0 +1,51 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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);
+
+UIntScale Scales::semitone(semitoneOffset + 48);
+LinearScale Scales::cent(-100.0, 100.0);
+LinearScale Scales::pitchBendRange(0.0, 120.0);
+DecibelScale Scales::noteSlideTimeSecond(-40.0, 40.0, false);
+
+DecibelScale Scales::noiseDecaySeconds(-100, 40, false);
+DecibelScale Scales::delayTimeSecond(-100, -20, false);
+DecibelScale Scales::delayTimeModAmount(-20, 60, true);
+
+DecibelScale Scales::hihatDistance(-80, 20, true);
+
+} // namespace Synth
+} // namespace Steinberg
diff --git a/LoopCymbal/source/parameter.hpp b/LoopCymbal/source/parameter.hpp
new file mode 100644
index 00000000..8409055c
--- /dev/null
+++ b/LoopCymbal/source/parameter.hpp
@@ -0,0 +1,230 @@
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. If not, see .
+
+#pragma once
+
+#include
+#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;
+
+constexpr size_t nReservedParameter = 64;
+constexpr size_t nReservedGuiParameter = 16;
+
+namespace Steinberg {
+namespace Synth {
+
+namespace ParameterID {
+enum ID {
+ bypass,
+
+ outputGain,
+ overSampling,
+ resetSeedAtNoteOn,
+
+ stereoBalance,
+ stereoMerge,
+
+ useExternalInput,
+ externalInputGain,
+
+ notePitchAmount,
+ tuningSemitone,
+ tuningCent,
+ pitchBend,
+ pitchBendRange,
+ noteSlideTimeSecond,
+
+ seed,
+ noiseDecaySeconds,
+
+ delayTimeBaseSecond,
+ delayTimeRandomSecond,
+ delayTimeModAmount,
+ allpassFeed,
+
+ hihatDistance,
+
+ reservedParameter0,
+ reservedGuiParameter0 = reservedParameter0 + nReservedParameter,
+
+ ID_ENUM_LENGTH = reservedGuiParameter0 + nReservedGuiParameter,
+ // ID_ENUM_GUI_START = reservedGuiParameter0,
+};
+} // 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::UIntScale semitone;
+ static SomeDSP::LinearScale cent;
+ static SomeDSP::LinearScale pitchBendRange;
+ static SomeDSP::DecibelScale noteSlideTimeSecond;
+
+ static SomeDSP::DecibelScale noiseDecaySeconds;
+ static SomeDSP::DecibelScale delayTimeSecond;
+ static SomeDSP::DecibelScale delayTimeModAmount;
+
+ static SomeDSP::DecibelScale hihatDistance;
+};
+
+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::overSampling] = std::make_unique(
+ 1, Scales::boolScale, "overSampling", Info::kCanAutomate);
+ value[ID::resetSeedAtNoteOn] = std::make_unique(
+ 0, Scales::boolScale, "resetSeedAtNoteOn", Info::kCanAutomate);
+
+ value[ID::stereoBalance] = std::make_unique(
+ Scales::bipolarScale.invmap(0.0), Scales::bipolarScale, "stereoBalance",
+ Info::kCanAutomate);
+ value[ID::stereoMerge] = std::make_unique(
+ Scales::defaultScale.invmap(0.75), Scales::defaultScale, "stereoMerge",
+ Info::kCanAutomate);
+
+ value[ID::useExternalInput] = std::make_unique(
+ 0, Scales::boolScale, "useExternalInput", Info::kCanAutomate);
+ value[ID::externalInputGain] = std::make_unique(
+ Scales::gain.invmap(1.0), Scales::gain, "externalInputGain", Info::kCanAutomate);
+
+ value[ID::notePitchAmount] = std::make_unique(
+ Scales::bipolarScale.invmap(0.0), Scales::bipolarScale, "notePitchAmount",
+ 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::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.1), Scales::noteSlideTimeSecond,
+ "noteSlideTimeSecond", Info::kCanAutomate);
+
+ value[ID::seed]
+ = std::make_unique(0, Scales::seed, "seed", Info::kCanAutomate);
+ value[ID::noiseDecaySeconds] = std::make_unique(
+ Scales::noiseDecaySeconds.invmap(0.001), Scales::noiseDecaySeconds,
+ "noiseDecaySeconds", Info::kCanAutomate);
+ value[ID::delayTimeBaseSecond] = std::make_unique(
+ Scales::delayTimeSecond.invmap(0.001), Scales::delayTimeSecond,
+ "delayTimeBaseSecond", Info::kCanAutomate);
+ value[ID::delayTimeRandomSecond] = std::make_unique(
+ Scales::delayTimeSecond.invmap(0.001), Scales::delayTimeSecond,
+ "delayTimeRandomSecond", Info::kCanAutomate);
+ value[ID::delayTimeModAmount] = std::make_unique(
+ Scales::delayTimeModAmount.invmap(0.0), Scales::delayTimeModAmount,
+ "delayTimeModAmount", Info::kCanAutomate);
+ value[ID::allpassFeed] = std::make_unique(
+ Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed",
+ Info::kCanAutomate);
+
+ value[ID::hihatDistance] = std::make_unique(
+ Scales::hihatDistance.invmap(10.0), Scales::hihatDistance, "hihatDistance",
+ Info::kCanAutomate);
+
+ for (size_t idx = 0; idx < nReservedParameter; ++idx) {
+ auto indexStr = std::to_string(idx);
+ value[ID::reservedParameter0 + idx] = std::make_unique(
+ Scales::defaultScale.invmap(1.0), Scales::defaultScale,
+ ("reservedParameter" + indexStr).c_str(), Info::kIsHidden);
+ }
+
+ for (size_t idx = 0; idx < nReservedGuiParameter; ++idx) {
+ auto indexStr = std::to_string(idx);
+ value[ID::reservedGuiParameter0 + idx] = std::make_unique(
+ Scales::defaultScale.invmap(1.0), Scales::defaultScale,
+ ("reservedGuiParameter" + indexStr).c_str(), Info::kIsHidden);
+ }
+
+ 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/LoopCymbal/source/plugfactory.cpp b/LoopCymbal/source/plugfactory.cpp
new file mode 100644
index 00000000..2560b387
--- /dev/null
+++ b/LoopCymbal/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 LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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::kFxInstrument
+
+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/LoopCymbal/source/plugprocessor.cpp b/LoopCymbal/source/plugprocessor.cpp
new file mode 100644
index 00000000..f34b97e3
--- /dev/null
+++ b/LoopCymbal/source/plugprocessor.cpp
@@ -0,0 +1,191 @@
+// Modified by:
+// (c) 2023 Takamitsu Endo
+//
+// This file is part of LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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;
+
+ addAudioInput(STR16("StereoInput"), Vst::SpeakerArr::kStereo);
+ 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.numInputs == 0) return kResultOk;
+ if (data.numOutputs == 0) return kResultOk;
+ if (data.numSamples <= 0) return kResultOk;
+ if (data.inputs[0].numChannels < 2) return kResultOk;
+ if (data.outputs[0].numChannels < 2) return kResultOk;
+ if (data.symbolicSampleSize == Vst::kSample64) return kResultOk;
+
+ if (data.inputEvents != nullptr) handleEvent(data);
+
+ const float *in0 = data.inputs[0].channelBuffers32[0];
+ const float *in1 = data.inputs[0].channelBuffers32[1];
+ float *out0 = data.outputs[0].channelBuffers32[0];
+ float *out1 = data.outputs[0].channelBuffers32[1];
+ dsp.process((size_t)data.numSamples, in0, in1, out0, out1);
+
+ // // Send parameter changes for GUI.
+ // if (!data.outputParameterChanges) return kResultOk;
+ // int32 index = 0;
+ // for (uint32 id = ID::ID_ENUM_GUI_START; id < ID::ID_ENUM_LENGTH; ++id) {
+ // auto queue = data.outputParameterChanges->addParameterData(id, index);
+ // if (!queue) continue;
+ // queue->addPoint(0, dsp.param.value[id]->getNormalized(), index);
+ // }
+
+ 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/LoopCymbal/source/plugprocessor.hpp b/LoopCymbal/source/plugprocessor.hpp
new file mode 100644
index 00000000..b65b5e1a
--- /dev/null
+++ b/LoopCymbal/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 LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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/LoopCymbal/source/version.hpp b/LoopCymbal/source/version.hpp
new file mode 100644
index 00000000..dae0b573
--- /dev/null
+++ b/LoopCymbal/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 LoopCymbal.
+//
+// LoopCymbal 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.
+//
+// LoopCymbal 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 LoopCymbal. 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 "LoopCymbal"
+
+#define stringOriginalFilename "LoopCymbal.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 2024 Takamitsu Endo"
+#define stringLegalTrademarks "VST is a trademark of Steinberg Media Technologies GmbH"
diff --git a/LoopCymbal/test/testdsp.cpp b/LoopCymbal/test/testdsp.cpp
new file mode 100644
index 00000000..d335b478
--- /dev/null
+++ b/LoopCymbal/test/testdsp.cpp
@@ -0,0 +1,36 @@
+// (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();
+#define HAS_INPUT 1
+#define NO_DSP_INTERFACE
+
+#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 "LoopCymbal"
+#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;
+}