From 761478d221f5314c1e826ae3f20aff33f3df558b Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Tue, 25 Jun 2024 20:41:10 +0900 Subject: [PATCH] Add LoopCymbal (WIP) --- LoopCymbal/CMakeLists.txt | 17 ++ LoopCymbal/resource/Info.plist | 28 ++ LoopCymbal/resource/plug.rc | 44 +++ LoopCymbal/source/controller.hpp | 85 ++++++ LoopCymbal/source/dsp/delay.hpp | 172 +++++++++++ LoopCymbal/source/dsp/dspcore.cpp | 332 ++++++++++++++++++++++ LoopCymbal/source/dsp/dspcore.hpp | 140 +++++++++ LoopCymbal/source/editor.cpp | 260 +++++++++++++++++ LoopCymbal/source/editor.hpp | 47 +++ LoopCymbal/source/fuid.hpp | 30 ++ LoopCymbal/source/gui/randomizebutton.hpp | 173 +++++++++++ LoopCymbal/source/gui/splashdraw.cpp | 126 ++++++++ LoopCymbal/source/parameter.cpp | 51 ++++ LoopCymbal/source/parameter.hpp | 230 +++++++++++++++ LoopCymbal/source/plugfactory.cpp | 73 +++++ LoopCymbal/source/plugprocessor.cpp | 191 +++++++++++++ LoopCymbal/source/plugprocessor.hpp | 68 +++++ LoopCymbal/source/version.hpp | 56 ++++ LoopCymbal/test/testdsp.cpp | 36 +++ 19 files changed, 2159 insertions(+) create mode 100644 LoopCymbal/CMakeLists.txt create mode 100644 LoopCymbal/resource/Info.plist create mode 100644 LoopCymbal/resource/plug.rc create mode 100644 LoopCymbal/source/controller.hpp create mode 100644 LoopCymbal/source/dsp/delay.hpp create mode 100644 LoopCymbal/source/dsp/dspcore.cpp create mode 100644 LoopCymbal/source/dsp/dspcore.hpp create mode 100644 LoopCymbal/source/editor.cpp create mode 100644 LoopCymbal/source/editor.hpp create mode 100644 LoopCymbal/source/fuid.hpp create mode 100644 LoopCymbal/source/gui/randomizebutton.hpp create mode 100644 LoopCymbal/source/gui/splashdraw.cpp create mode 100644 LoopCymbal/source/parameter.cpp create mode 100644 LoopCymbal/source/parameter.hpp create mode 100644 LoopCymbal/source/plugfactory.cpp create mode 100644 LoopCymbal/source/plugprocessor.cpp create mode 100644 LoopCymbal/source/plugprocessor.hpp create mode 100644 LoopCymbal/source/version.hpp create mode 100644 LoopCymbal/test/testdsp.cpp 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; +}