From 12086f2c817d47a81f8a63a8e78a7fb7a0098099 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Sun, 18 Aug 2024 00:43:41 +0900 Subject: [PATCH] Update LoopCymbal (WIP) --- CMakeLists.txt | 86 ++++++++------- LoopCymbal/source/dsp/delay.hpp | 99 ++++++++++++++++- LoopCymbal/source/dsp/dspcore.cpp | 175 ++++++++++++++++++------------ LoopCymbal/source/dsp/dspcore.hpp | 26 +++-- LoopCymbal/source/editor.cpp | 78 +++++++++++-- LoopCymbal/source/parameter.cpp | 9 +- LoopCymbal/source/parameter.hpp | 57 +++++++--- 7 files changed, 368 insertions(+), 162 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47917a12..05bfc1fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) project(UhhyouPlugins - VERSION 0.61.0) + VERSION 0.63.0) option(SMTG_ADD_VST3_HOSTING_SAMPLES OFF) option(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF) @@ -16,47 +16,49 @@ add_subdirectory(common) add_subdirectory(lib/vst3sdk) smtg_enable_vst3_sdk() -add_subdirectory(AccumulativeRingMod) -add_subdirectory(BasicLimiter) -add_subdirectory(BasicLimiterAutoMake) -add_subdirectory(ClangCymbal) -add_subdirectory(ClangSynth) -add_subdirectory(CollidingCombSynth) -add_subdirectory(CombDistortion) -add_subdirectory(CubicPadSynth) -add_subdirectory(EnvelopedSine) -add_subdirectory(EsPhaser) -add_subdirectory(FDN64Reverb) -add_subdirectory(FDNCymbal) -add_subdirectory(FeedbackPhaser) -add_subdirectory(FoldShaper) -add_subdirectory(GenericDrum) -add_subdirectory(GlitchSprinkler) -add_subdirectory(IterativeSinCluster) -add_subdirectory(L3Reverb) -add_subdirectory(L4Reverb) -add_subdirectory(LatticeReverb) -add_subdirectory(LightPadSynth) -add_subdirectory(LongPhaser) -add_subdirectory(MatrixShifter) -add_subdirectory(MaybeSnare) -add_subdirectory(MembraneSynth) -add_subdirectory(MiniCliffEQ) -add_subdirectory(ModuloShaper) -add_subdirectory(NarrowingDelay) -add_subdirectory(OddPowShaper) -add_subdirectory(OrdinaryPhaser) -add_subdirectory(ParallelComb) -add_subdirectory(ParallelDetune) -add_subdirectory(PitchShiftDelay) -add_subdirectory(RingModSpacer) -add_subdirectory(SevenDelay) -add_subdirectory(SoftClipper) -add_subdirectory(SyncSawSynth) -add_subdirectory(TrapezoidSynth) -add_subdirectory(UltraSynth) -add_subdirectory(UltrasonicRingMod) -add_subdirectory(WaveCymbal) +# add_subdirectory(AccumulativeRingMod) +# add_subdirectory(BasicLimiter) +# add_subdirectory(BasicLimiterAutoMake) +# add_subdirectory(ClangCymbal) +# add_subdirectory(ClangSynth) +# add_subdirectory(CollidingCombSynth) +# add_subdirectory(CombDistortion) +# add_subdirectory(CubicPadSynth) +# add_subdirectory(EnvelopedSine) +# add_subdirectory(EsPhaser) +# add_subdirectory(FDN64Reverb) +# add_subdirectory(FDNCymbal) +# add_subdirectory(FeedbackPhaser) +# add_subdirectory(FoldShaper) +# add_subdirectory(GenericDrum) +# add_subdirectory(GlitchSprinkler) +# add_subdirectory(IterativeSinCluster) +# add_subdirectory(L3Reverb) +# add_subdirectory(L4Reverb) +# add_subdirectory(LatticeReverb) +# add_subdirectory(LightPadSynth) +# add_subdirectory(LongPhaser) +add_subdirectory(LoopCymbal) + +# add_subdirectory(MatrixShifter) +# add_subdirectory(MaybeSnare) +# add_subdirectory(MembraneSynth) +# add_subdirectory(MiniCliffEQ) +# add_subdirectory(ModuloShaper) +# add_subdirectory(NarrowingDelay) +# add_subdirectory(OddPowShaper) +# add_subdirectory(OrdinaryPhaser) +# add_subdirectory(ParallelComb) +# add_subdirectory(ParallelDetune) +# add_subdirectory(PitchShiftDelay) +# add_subdirectory(RingModSpacer) +# add_subdirectory(SevenDelay) +# add_subdirectory(SoftClipper) +# add_subdirectory(SyncSawSynth) +# add_subdirectory(TrapezoidSynth) +# add_subdirectory(UltraSynth) +# add_subdirectory(UltrasonicRingMod) +# add_subdirectory(WaveCymbal) # ## Below are prototype plugins. Breaking changes will be introduced. # add_subdirectory(TestBedSynth) diff --git a/LoopCymbal/source/dsp/delay.hpp b/LoopCymbal/source/dsp/delay.hpp index 44bc0bc3..53280b15 100644 --- a/LoopCymbal/source/dsp/delay.hpp +++ b/LoopCymbal/source/dsp/delay.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -30,14 +31,14 @@ template class ExpDecay { Sample alpha = Sample(0); public: - void setTime(Sample decayTimeInSamples) + void setTime(Sample decayTimeInSamples, bool sustain = false) { - alpha = std::pow( - Sample(std::numeric_limits::epsilon()), Sample(1) / decayTimeInSamples); + constexpr auto eps = Sample(std::numeric_limits::epsilon()); + alpha = sustain ? Sample(1) : std::pow(eps, Sample(1) / decayTimeInSamples); } void reset() { value = Sample(0); } - void noteOn() { value = Sample(1); } + void noteOn(Sample gain = Sample(1)) { value = gain; } Sample process() { return value *= alpha; } }; @@ -89,6 +90,59 @@ template class Delay { } }; +/** +Unused. This is a drop in replacement of `Delay`, but doubles the CPU load. + +- Sustain of the sound becomes shorter. Perhaps second allpass loop is required. +- Rotating `phase` at right frequency makes better sound. It adds artifact due to AM. +- `inputRatio` is better to be controled by user. +*/ +template class FDN2 { +public: + static constexpr size_t size = 2; + + Sample phase = 0; + std::array buffer; + std::array, size> delay; + + void setup(Sample maxTimeSamples) + { + for (auto &x : delay) x.setup(maxTimeSamples); + } + + void reset() + { + buffer.fill({}); + for (auto &x : delay) x.reset(); + } + + Sample process(Sample input, Sample timeInSamples) + { + // Fixed parameters. [a, b] is the range of the value. + constexpr auto phaseRatio = Sample(1) / Sample(3); // [positive small, +inf]. + constexpr auto feedback = Sample(1); // [-1, 1]. + constexpr auto timeRatio = Sample(4) / Sample(3); // [0, +inf]. + constexpr auto inputRatio = Sample(0.5); // [0, 1]. + + constexpr auto twopi = Sample(2) * std::numbers::pi_v; + const auto cs = std::cos(twopi * phase); + const auto sn = std::sin(twopi * phase); + + phase += phaseRatio / timeInSamples; + phase -= std::floor(phase); + + const auto sig0 = feedback * (cs * buffer[0] - sn * buffer[1]); + const auto sig1 = feedback * (sn * buffer[0] + cs * buffer[1]); + + constexpr auto g1 = inputRatio; + constexpr auto g2 = Sample(1) - inputRatio; + buffer[0] = delay[0].process(g1 * input + sig0, timeInSamples); + buffer[1] = delay[1].process(g2 * input + sig1, timeInSamples * timeRatio); + + return buffer[0] + buffer[1]; + } +}; + // Unused. `SerialAllpass` became unstable when used. template class NyquistLowpass { private: @@ -129,11 +183,26 @@ template class EmaHighShelf { } }; +template class EmaLowShelf { +private: + Sample value = 0; + +public: + void reset() { value = 0; } + + Sample process(Sample input, Sample kp, Sample shelvingGain) + { + value += kp * (input - value); + return std::lerp(input - value, input, shelvingGain); + } +}; + template class SerialAllpass { private: std::array buffer{}; std::array, nAllpass> delay; std::array, nAllpass> lowpass; + std::array, nAllpass> highpass; public: static constexpr size_t size = nAllpass; @@ -149,22 +218,40 @@ template class SerialAllpass { buffer.fill({}); for (auto &x : delay) x.reset(); for (auto &x : lowpass) x.reset(); + for (auto &x : highpass) x.reset(); + } + + Sample sum(Sample altSignMix) + { + Sample sumAlt = Sample(0); + Sample sign = Sample(1); + for (const auto &x : buffer) { + sumAlt += x * sign; + sign = -sign; + } + Sample sumDirect = std::accumulate(buffer.begin(), buffer.end(), Sample(0)); + return std::lerp(sumDirect, sumAlt, altSignMix) / (Sample(2) * nAllpass); } Sample process( Sample input, Sample highShelfCut, Sample highShelfGain, + Sample lowShelfCut, + Sample lowShelfGain, Sample gain, + Sample pitchRatio, Sample timeModAmount) { for (size_t idx = 0; idx < nAllpass; ++idx) { auto x0 = lowpass[idx].process(input, highShelfCut, highShelfGain); + x0 = highpass[idx].process(x0, lowShelfCut, lowShelfGain); x0 -= gain * buffer[idx]; input = buffer[idx] + gain * x0; - buffer[idx] - = delay[idx].process(x0, timeInSamples[idx] - timeModAmount * std::abs(x0)); + buffer[idx] = delay[idx].process( + x0, timeInSamples[idx] / pitchRatio - timeModAmount * std::abs(x0)); } + // buffer.back() = highpass.process(buffer.back(), lowShelfCut, lowShelfGain); return input; } }; diff --git a/LoopCymbal/source/dsp/dspcore.cpp b/LoopCymbal/source/dsp/dspcore.cpp index e41b7898..2a7b8805 100644 --- a/LoopCymbal/source/dsp/dspcore.cpp +++ b/LoopCymbal/source/dsp/dspcore.cpp @@ -1,4 +1,4 @@ -// (c) 2023 Takamitsu Endo +// (c) 2024 Takamitsu Endo // // This file is part of LoopCymbal. // @@ -25,16 +25,7 @@ #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); -} +constexpr double releaseTimeSecond = double(0.05); template inline auto prepareSerialAllpassTime(double upRate, double allpassMaxTimeHz, Rng &rng) @@ -77,7 +68,8 @@ void DSPCore::setup(double sampleRate) SmootherCommon::setTime(double(0.2)); const auto maxDelayTimeSamples = upRate * 2 * Scales::delayTimeSecond.getMax(); - for (auto &x : serialAllpass) x.setup(maxDelayTimeSamples); + for (auto &x : serialAllpass1) x.setup(maxDelayTimeSamples); + for (auto &x : serialAllpass2) x.setup(maxDelayTimeSamples); reset(); startup(); @@ -91,34 +83,36 @@ void DSPCore::setup(double sampleRate) \ pitchSmoothingKp \ = EMAFilter::secondToP(upRate, pv[ID::noteSlideTimeSecond]->getDouble()); \ - auto pitchBend \ - = calcPitch(pv[ID::pitchBendRange]->getDouble() * pv[ID::pitchBend]->getDouble()); \ - auto notePitch = calcNotePitch(pitchBend * noteNumber); \ + auto notePitch = calcNotePitch(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()); \ + allpassFeed1.METHOD( \ + std::clamp(pv[ID::allpassFeed1]->getDouble(), double(-0.99999), double(0.99999))); \ + allpassFeed2.METHOD( \ + std::clamp(pv[ID::allpassFeed2]->getDouble(), double(-0.99999), double(0.99999))); \ + allpassMixSpike.METHOD(pv[ID::allpassMixSpike]->getDouble()); \ + allpassMixAltSign.METHOD(pv[ID::allpassMixAltSign]->getDouble()); \ + highShelfCutoff.METHOD( \ + EMAFilter::cutoffToP(upRate, pv[ID::highShelfFrequencyHz]->getDouble())); \ + highShelfGain.METHOD(pv[ID::highShelfGain]->getDouble()); \ + lowShelfCutoff.METHOD( \ + EMAFilter::cutoffToP(upRate, pv[ID::lowShelfFrequencyHz]->getDouble())); \ + lowShelfGain.METHOD(pv[ID::lowShelfGain]->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); \ + envelopeNoise.setTime(pv[ID::noiseDecaySeconds]->getDouble() * upRate); \ + if (!pv[ID::release]->getInt() && noteStack.empty()) { \ + envelopeRelease.setTime(releaseTimeSecond *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); \ - } \ - } + updateDelayTime(); void DSPCore::updateUpRate() { @@ -126,6 +120,25 @@ void DSPCore::updateUpRate() SmootherCommon::setSampleRate(upRate); } +void DSPCore::updateDelayTime() +{ + using ID = ParameterID::ID; + const auto &pv = param.value; + + 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 (size_t idx = 0; idx < nAllpass; ++idx) { + const auto timeHarmonics = delayTimeBase / double(idx + 1); + serialAllpass1[0].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); + serialAllpass1[1].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); + serialAllpass2[0].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); + serialAllpass2[1].timeInSamples[idx] = timeHarmonics + delayTimeDist(paramRng); + } +} + void DSPCore::reset() { noteNumber = 57.0; @@ -139,11 +152,12 @@ void DSPCore::reset() startup(); impulse = 0; - noiseGain = 0; - noiseDecay = 0; - envelope.reset(); - feedbackBuffer.fill(double(0)); - for (auto &x : serialAllpass) x.reset(); + envelopeNoise.reset(); + envelopeRelease.reset(); + feedbackBuffer1.fill(double(0)); + feedbackBuffer2.fill(double(0)); + for (auto &x : serialAllpass1) x.reset(); + for (auto &x : serialAllpass2) x.reset(); for (auto &x : halfbandInput) x.fill({}); for (auto &x : halfbandIir) x.reset(); @@ -166,33 +180,32 @@ void DSPCore::setParameters() 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 envRelease = envelopeRelease.process(); + const auto extGain = externalInputGain.process(); const auto timeModAmt = delayTimeModAmount.process(); - const auto apGain = allpassFeed.process(); - const auto distance = hihatDistance.process(); + + auto apGain1 = allpassFeed1.process(); + auto apGain2 = allpassFeed2.process(); + apGain1 = std::lerp(apGain1 * double(0.5), apGain1, envRelease); + apGain2 = std::lerp(apGain2 * double(0.5), apGain2, envRelease); + + const auto apMixSpike = allpassMixSpike.process(); + const auto apMixSign = allpassMixAltSign.process(); + const auto hsCut = highShelfCutoff.process(); + const auto hsGain = highShelfGain.process() * envRelease; + const auto lsCut = lowShelfCutoff.process(); + const auto lsGain = lowShelfGain.process(); const auto balance = stereoBalance.process(); const auto merge = stereoMerge.process(); - const auto outGain = outputGain.process(); + const auto outGain = outputGain.process() * envRelease; std::uniform_real_distribution dist{double(-1), double(1)}; - const auto noiseEnv = envelope.process(); + const auto noiseEnv = envelopeNoise.process(); std::array excitation{ - -apGain * feedbackBuffer[0], -apGain * feedbackBuffer[1]}; + -apGain1 * feedbackBuffer1[0], -apGain1 * feedbackBuffer1[1]}; if (impulse != 0) { excitation[0] += impulse; excitation[1] += impulse; @@ -207,17 +220,34 @@ std::array DSPCore::processFrame(const std::array &externa 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); + // Normalize amplitude. + const auto pitchRatio = interpPitch.process(pitchSmoothingKp); + const auto normalizeGain = nAllpass + * std::lerp(double(1) / std::sqrt(hsCut / (double(2) - hsCut)), hsGain, hsGain); + auto ap1Out0 + = std::lerp(serialAllpass1[0].sum(apMixSign), feedbackBuffer1[0], apMixSpike) + * normalizeGain; + auto ap1Out1 + = std::lerp(serialAllpass1[1].sum(apMixSign), feedbackBuffer1[1], apMixSpike) + * normalizeGain; + + feedbackBuffer1[0] = serialAllpass1[0].process( + excitation[0], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt); + feedbackBuffer1[1] = serialAllpass1[1].process( + excitation[1], hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt); + + auto cymbal0 + = std::lerp(serialAllpass2[0].sum(apMixSign), feedbackBuffer2[0], apMixSpike) + * normalizeGain; + auto cymbal1 + = std::lerp(serialAllpass2[1].sum(apMixSign), feedbackBuffer2[1], apMixSpike) + * normalizeGain; + feedbackBuffer2[0] = serialAllpass2[0].process( + ap1Out0 - apGain2 * feedbackBuffer2[0], hsCut, hsGain, lsCut, lsGain, apGain2, + pitchRatio, timeModAmt); + feedbackBuffer2[1] = serialAllpass2[1].process( + ap1Out1 - apGain2 * feedbackBuffer2[1], hsCut, hsGain, lsCut, lsGain, apGain2, + pitchRatio, timeModAmt); constexpr auto eps = std::numeric_limits::epsilon(); if (balance < -eps) { @@ -287,20 +317,16 @@ void DSPCore::noteOn(NoteInfo &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(); + impulse = velocity; + envelopeNoise.noteOn(velocity); + envelopeRelease.noteOn(double(1)); + envelopeRelease.setTime(1, true); } void DSPCore::noteOff(int_fast32_t noteId) @@ -317,6 +343,8 @@ void DSPCore::noteOff(int_fast32_t noteId) if (!noteStack.empty()) { noteNumber = noteStack.back().noteNumber; interpPitch.push(calcNotePitch(noteNumber)); + } else { + if (!pv[ID::release]->getInt()) envelopeRelease.setTime(releaseTimeSecond * upRate); } } @@ -325,8 +353,11 @@ double DSPCore::calcNotePitch(double note) using ID = ParameterID::ID; auto &pv = param.value; - auto semitone = pv[ID::tuningSemitone]->getInt() - double(semitoneOffset + 57); + constexpr auto centerNote = double(60); + auto pitchBendCent + = pv[ID::pitchBendRange]->getDouble() * pv[ID::pitchBend]->getDouble() / double(1200); auto cent = pv[ID::tuningCent]->getDouble() / double(100); auto notePitchAmount = pv[ID::notePitchAmount]->getDouble(); - return std::exp2(notePitchAmount * (note + semitone + cent) / double(12)); + return std::exp2( + pitchBendCent + (notePitchAmount * (note - centerNote) + cent) / double(12)); } diff --git a/LoopCymbal/source/dsp/dspcore.hpp b/LoopCymbal/source/dsp/dspcore.hpp index eb6f6ec7..8035aa96 100644 --- a/LoopCymbal/source/dsp/dspcore.hpp +++ b/LoopCymbal/source/dsp/dspcore.hpp @@ -1,4 +1,4 @@ -// (c) 2023 Takamitsu Endo +// (c) 2024 Takamitsu Endo // // This file is part of LoopCymbal. // @@ -94,6 +94,7 @@ class DSPCore { private: void updateUpRate(); + void updateDelayTime(); double calcNotePitch(double note); std::array processFrame(const std::array &externalInput); @@ -116,24 +117,29 @@ class DSPCore { ExpSmoother externalInputGain; ExpSmoother delayTimeModAmount; - ExpSmoother allpassFeed; - ExpSmoother hihatDistance; + ExpSmoother allpassFeed1; + ExpSmoother allpassFeed2; + ExpSmoother allpassMixSpike; + ExpSmoother allpassMixAltSign; + ExpSmoother highShelfCutoff; + ExpSmoother highShelfGain; + ExpSmoother lowShelfCutoff; + ExpSmoother lowShelfGain; 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; + ExpDecay envelopeNoise; + ExpDecay envelopeRelease; + std::array feedbackBuffer1{}; + std::array feedbackBuffer2{}; + std::array, 2> serialAllpass1; + std::array, 2> serialAllpass2; std::array, 2> halfbandInput{}; std::array>, 2> halfbandIir; diff --git a/LoopCymbal/source/editor.cpp b/LoopCymbal/source/editor.cpp index a276b70c..993449ad 100644 --- a/LoopCymbal/source/editor.cpp +++ b/LoopCymbal/source/editor.cpp @@ -129,6 +129,8 @@ bool Editor::prepareUI() addCheckbox( mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Reset Seed at Note-on", ID::resetSeedAtNoteOn); + addCheckbox( + mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, "Release", ID::release); addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Stereo Balance"); addTextKnob( @@ -163,20 +165,17 @@ bool Editor::prepareUI() 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"); + addLabel( + tuningLeft0, tuningTop3, labelWidth, labelHeight, uiTextSize, "Transpose [cent]"); addTextKnob( tuningLeft1, tuningTop3, labelWidth, labelHeight, uiTextSize, ID::tuningCent, Scales::cent, false, 5); addLabel( tuningLeft0, tuningTop4, labelWidth, labelHeight, uiTextSize, - "Pitch Bend Range [st.]"); + "Pitch Bend Range [cent]"); addTextKnob( tuningLeft1, tuningTop4, labelWidth, labelHeight, uiTextSize, ID::pitchBendRange, - Scales::pitchBendRange, false, 5); + Scales::cent, false, 5); addLabel( tuningLeft0, tuningTop5, labelWidth, labelHeight, uiTextSize, "Slide Time [s]"); addTextKnob( @@ -192,6 +191,12 @@ bool Editor::prepareUI() constexpr auto impactTop5 = impactTop0 + 5 * labelY; constexpr auto impactTop6 = impactTop0 + 6 * labelY; constexpr auto impactTop7 = impactTop0 + 7 * labelY; + constexpr auto impactTop8 = impactTop0 + 8 * labelY; + constexpr auto impactTop9 = impactTop0 + 9 * labelY; + constexpr auto impactTop10 = impactTop0 + 10 * labelY; + constexpr auto impactTop11 = impactTop0 + 11 * labelY; + constexpr auto impactTop12 = impactTop0 + 12 * labelY; + constexpr auto impactTop13 = impactTop0 + 13 * labelY; constexpr auto impactLeft0 = left4; constexpr auto impactLeft1 = impactLeft0 + labelWidth + 2 * margin; addGroupLabel( @@ -227,12 +232,63 @@ bool Editor::prepareUI() Scales::delayTimeModAmount, false, 5); addLabel(impactLeft0, impactTop6, labelWidth, labelHeight, uiTextSize, "Feed"); addTextKnob( - impactLeft1, impactTop6, labelWidth, labelHeight, uiTextSize, ID::allpassFeed, + impactLeft1, impactTop6, labelWidth, labelHeight, uiTextSize, ID::allpassFeed1, Scales::bipolarScale, false, 5); - addLabel(impactLeft0, impactTop7, labelWidth, labelHeight, uiTextSize, "Distance"); + + addLabel( + impactLeft0, impactTop8, labelWidth, labelHeight, uiTextSize, + "High Shelf Cutoff [Hz]"); + addTextKnob( + impactLeft1, impactTop8, labelWidth, labelHeight, uiTextSize, + ID::highShelfFrequencyHz, Scales::cutoffFrequencyHz, false, 5); + addLabel( + impactLeft0, impactTop9, labelWidth, labelHeight, uiTextSize, "High Shelf Gain [dB]"); + addTextKnob( + impactLeft1, impactTop9, labelWidth, labelHeight, uiTextSize, ID::highShelfGain, + Scales::shelvingGain, true, 5); + addLabel( + impactLeft0, impactTop10, labelWidth, labelHeight, uiTextSize, + "Low Shelf Cutoff [Hz]"); + addTextKnob( + impactLeft1, impactTop10, labelWidth, labelHeight, uiTextSize, + ID::lowShelfFrequencyHz, Scales::cutoffFrequencyHz, false, 5); + addLabel( + impactLeft0, impactTop11, labelWidth, labelHeight, uiTextSize, "Low Shelf Gain [dB]"); + addTextKnob( + impactLeft1, impactTop11, labelWidth, labelHeight, uiTextSize, ID::lowShelfGain, + Scales::shelvingGain, true, 5); + addLabel(impactLeft0, impactTop12, labelWidth, labelHeight, uiTextSize, "Mix Spike"); + addTextKnob( + impactLeft1, impactTop12, labelWidth, labelHeight, uiTextSize, ID::allpassMixSpike, + Scales::defaultScale, false, 5); + addLabel( + impactLeft0, impactTop13, labelWidth, labelHeight, uiTextSize, "Mix Alt. Sign"); addTextKnob( - impactLeft1, impactTop7, labelWidth, labelHeight, uiTextSize, ID::hihatDistance, - Scales::hihatDistance, false, 5); + impactLeft1, impactTop13, labelWidth, labelHeight, uiTextSize, ID::allpassMixAltSign, + Scales::defaultScale, false, 5); + + // Secondary. + constexpr auto secondTop0 = top0 + 0 * labelY; + constexpr auto secondTop1 = secondTop0 + 1 * labelY; + constexpr auto secondTop2 = secondTop0 + 2 * labelY; + constexpr auto secondTop3 = secondTop0 + 3 * labelY; + constexpr auto secondTop4 = secondTop0 + 4 * labelY; + constexpr auto secondTop5 = secondTop0 + 5 * labelY; + constexpr auto secondTop6 = secondTop0 + 6 * labelY; + constexpr auto secondTop7 = secondTop0 + 7 * labelY; + constexpr auto secondTop8 = secondTop0 + 8 * labelY; + constexpr auto secondTop9 = secondTop0 + 9 * labelY; + constexpr auto secondTop10 = secondTop0 + 10 * labelY; + constexpr auto secondTop11 = secondTop0 + 11 * labelY; + constexpr auto secondLeft0 = left8; + constexpr auto secondLeft1 = secondLeft0 + labelWidth + 2 * margin; + addGroupLabel( + secondLeft0, secondTop0, groupLabelWidth, labelHeight, uiTextSize, "Loop 2"); + + addLabel(secondLeft0, secondTop6, labelWidth, labelHeight, uiTextSize, "Feed"); + addTextKnob( + secondLeft1, secondTop6, labelWidth, labelHeight, uiTextSize, ID::allpassFeed2, + Scales::bipolarScale, false, 5); // Randomize button. const auto randomButtonTop = top0 + 18 * labelY; diff --git a/LoopCymbal/source/parameter.cpp b/LoopCymbal/source/parameter.cpp index 47e1cff1..eda4d4ec 100644 --- a/LoopCymbal/source/parameter.cpp +++ b/LoopCymbal/source/parameter.cpp @@ -36,15 +36,16 @@ 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); +LinearScale Scales::cent(-6000.0, 6000.0); +DecibelScale Scales::noteSlideTimeSecond(-100.0, 40.0, true); DecibelScale Scales::noiseDecaySeconds(-100, 40, false); DecibelScale Scales::delayTimeSecond(-100, -20, false); DecibelScale Scales::delayTimeModAmount(-20, 60, true); +DecibelScale Scales::cutoffFrequencyHz(0, 100, false); +DecibelScale Scales::shelvingGain(-60, 0, true); + DecibelScale Scales::hihatDistance(-80, 20, true); } // namespace Synth diff --git a/LoopCymbal/source/parameter.hpp b/LoopCymbal/source/parameter.hpp index 8409055c..1820901e 100644 --- a/LoopCymbal/source/parameter.hpp +++ b/LoopCymbal/source/parameter.hpp @@ -31,9 +31,7 @@ #include "../../common/value.hpp" #endif -constexpr int octaveOffset = 8; -constexpr int semitoneOffset = 96; -constexpr size_t maxFdnSize = 5; +constexpr size_t nAllpass = 16; constexpr size_t nReservedParameter = 64; constexpr size_t nReservedGuiParameter = 16; @@ -48,6 +46,7 @@ enum ID { outputGain, overSampling, resetSeedAtNoteOn, + release, stereoBalance, stereoMerge, @@ -56,7 +55,6 @@ enum ID { externalInputGain, notePitchAmount, - tuningSemitone, tuningCent, pitchBend, pitchBendRange, @@ -68,9 +66,15 @@ enum ID { delayTimeBaseSecond, delayTimeRandomSecond, delayTimeModAmount, - allpassFeed, + allpassFeed1, + allpassFeed2, + allpassMixSpike, + allpassMixAltSign, - hihatDistance, + highShelfFrequencyHz, + highShelfGain, + lowShelfFrequencyHz, + lowShelfGain, reservedParameter0, reservedGuiParameter0 = reservedParameter0 + nReservedParameter, @@ -88,15 +92,16 @@ struct Scales { 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 cutoffFrequencyHz; + static SomeDSP::DecibelScale shelvingGain; + static SomeDSP::DecibelScale hihatDistance; }; @@ -122,6 +127,8 @@ struct GlobalParameter : public ParameterInterface { 1, Scales::boolScale, "overSampling", Info::kCanAutomate); value[ID::resetSeedAtNoteOn] = std::make_unique( 0, Scales::boolScale, "resetSeedAtNoteOn", Info::kCanAutomate); + value[ID::release] + = std::make_unique(1, Scales::boolScale, "release", Info::kCanAutomate); value[ID::stereoBalance] = std::make_unique( Scales::bipolarScale.invmap(0.0), Scales::bipolarScale, "stereoBalance", @@ -138,17 +145,14 @@ struct GlobalParameter : public ParameterInterface { 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); + Scales::cent.invmap(1200.0), Scales::cent, "pitchBendRange", Info::kCanAutomate); value[ID::noteSlideTimeSecond] = std::make_unique( - Scales::noteSlideTimeSecond.invmap(0.1), Scales::noteSlideTimeSecond, + Scales::noteSlideTimeSecond.invmap(0.0001), Scales::noteSlideTimeSecond, "noteSlideTimeSecond", Info::kCanAutomate); value[ID::seed] @@ -162,15 +166,34 @@ struct GlobalParameter : public ParameterInterface { 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", + value[ID::allpassFeed1] = std::make_unique( + Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed1", + Info::kCanAutomate); + value[ID::allpassFeed2] = std::make_unique( + Scales::bipolarScale.invmap(0.98), Scales::bipolarScale, "allpassFeed2", + Info::kCanAutomate); + value[ID::allpassMixSpike] = std::make_unique( + Scales::defaultScale.invmap(2.0 / 3.0), Scales::defaultScale, "allpassMixSpike", + Info::kCanAutomate); + value[ID::allpassMixAltSign] = std::make_unique( + Scales::defaultScale.invmap(0), Scales::defaultScale, "allpassMixAltSign", Info::kCanAutomate); - value[ID::hihatDistance] = std::make_unique( - Scales::hihatDistance.invmap(10.0), Scales::hihatDistance, "hihatDistance", + value[ID::highShelfFrequencyHz] = std::make_unique( + Scales::cutoffFrequencyHz.invmap(8000.0), Scales::cutoffFrequencyHz, + "highShelfFrequencyHz", Info::kCanAutomate); + value[ID::highShelfGain] = std::make_unique( + Scales::shelvingGain.invmapDB(-1.0), Scales::shelvingGain, "highShelfGain", + Info::kCanAutomate); + value[ID::lowShelfFrequencyHz] = std::make_unique( + Scales::cutoffFrequencyHz.invmap(20.0), Scales::cutoffFrequencyHz, + "lowShelfFrequencyHz", Info::kCanAutomate); + value[ID::lowShelfGain] = std::make_unique( + Scales::shelvingGain.invmapDB(-1.0), Scales::shelvingGain, "lowShelfGain", Info::kCanAutomate); for (size_t idx = 0; idx < nReservedParameter; ++idx) {