diff --git a/DoubleLoopCymbal/source/dsp/delay.hpp b/DoubleLoopCymbal/source/dsp/delay.hpp index 43f74272..264891c2 100644 --- a/DoubleLoopCymbal/source/dsp/delay.hpp +++ b/DoubleLoopCymbal/source/dsp/delay.hpp @@ -68,6 +68,8 @@ template class ExpDSREnvelope { enum class State { decay, release }; private: + static constexpr auto eps = Sample(std::numeric_limits::epsilon()); + Sample timeD = Sample(1); Sample value = 0; Sample alphaD = 0; Sample alphaR = 0; @@ -77,8 +79,8 @@ template class ExpDSREnvelope { public: void setTime(Sample decayTimeInSamples, Sample releaseTimeInSamples) { - constexpr auto eps = Sample(std::numeric_limits::epsilon()); - alphaD = std::pow(eps, Sample(1) / decayTimeInSamples); + // alphaD = std::pow(eps, Sample(1) / decayTimeInSamples); + timeD = decayTimeInSamples; alphaR = std::pow(eps, Sample(1) / releaseTimeInSamples); } @@ -91,10 +93,12 @@ template class ExpDSREnvelope { state = State::release; } - void trigger(Sample sustainLevel) + // `decayScaler` must be greater than 0. + void trigger(Sample sustainLevel, Sample decayScaler) { state = State::decay; value = Sample(1) - sustainLevel; + alphaD = std::pow(eps, Sample(1) / (timeD * decayScaler)); offset = sustainLevel; } @@ -446,4 +450,63 @@ template class HalfClosedNoise { } }; +// Stereo spreader. +// 2-band splitter is made from bilinear transformed 1-pole filters. +template class Spreader { +private: + Sample x1 = 0; + Sample y1Lp = 0; + Sample y2Lp = 0; + Sample y1Hp = 0; + Sample y2Hp = 0; + + Sample baseTime = double(1); + Delay delay; + +public: + void setup(Sample maxTimeSample) { delay.setup(maxTimeSample); } + void updateBaseTime(Sample maxTimeSample) { baseTime = Sample(0.5) * maxTimeSample; } + + void reset() + { + x1 = 0; + y1Lp = 0; + y2Lp = 0; + y1Hp = 0; + y2Hp = 0; + + delay.reset(); + } + + std::array process(Sample input, Sample splitFreqNormalized, Sample spread) + { + // 2-band splitter. + constexpr auto minFreq = Sample(0.00001); + constexpr auto nyquist = Sample(0.49998); + const auto cut = std::clamp(splitFreqNormalized, minFreq, nyquist); + const auto kp = Sample(1) / std::tan(std::numbers::pi_v * cut); + const auto a0 = Sample(1) + kp; + const auto bLp = Sample(1) / a0; + const auto bHp = kp / a0; + const auto a1 = (Sample(1) - kp) / a0; + + const auto x2Lp = y1Lp; + y1Lp = bLp * (input + x1) - a1 * y1Lp; + y2Lp = bLp * (y1Lp + x2Lp) - a1 * y2Lp; + + const auto x2Hp = y1Hp; + y1Hp = bHp * (input - x1) - a1 * y1Hp; + y2Hp = bHp * (y1Hp - x2Hp) - a1 * y2Hp; + + x1 = input; + + // Spared stereo with delay. + constexpr auto sqrt2 = std::numbers::sqrt2_v; + const auto delayed + = delay.process(y2Hp * spread / (-sqrt2), (spread + Sample(1)) * baseTime); + const auto merged = y2Lp - y2Hp; + return {merged + delayed, merged - delayed}; + } +}; + } // namespace SomeDSP diff --git a/DoubleLoopCymbal/source/dsp/dspcore.cpp b/DoubleLoopCymbal/source/dsp/dspcore.cpp index 8c877cd5..77ba6957 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.cpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.cpp @@ -27,6 +27,15 @@ constexpr double defaultTempo = double(120); constexpr double releaseTimeSecond = double(4.0); constexpr double closeReleaseSecond = double(0.5); +constexpr double spreaderMaxTimeSecond = double(0.006); + +// `value` in [0, 1]. +template inline T decibelMap(T value, T minDB, T maxDB, bool minToZero) +{ + if (minToZero && value <= T(0)) return T(0); + T dB = std::clamp(value * (maxDB - minDB) + minDB, minDB, maxDB); + return std::pow(T(10), dB / T(20)); +} constexpr const std::array circularModes = { double(1.000000000000000), double(1.5933405056951118), double(2.135548786649403), @@ -156,13 +165,16 @@ void DSPCore::setup(double sampleRate) upRate = sampleRate * upFold; SmootherCommon::setTime(double(0.2)); + baseSampleRateKp = EMAFilter::secondToP(sampleRate, double(0.2)); releaseSmoother.setup(double(2) * upRate); envelopeClose.setup(EMAFilter::secondToP(upRate, double(0.004))); const auto maxDelayTimeSamples = upRate * 2 * Scales::delayTimeSecond.getMax(); - for (auto &x : serialAllpass1) x.setup(maxDelayTimeSamples); - for (auto &x : serialAllpass2) x.setup(maxDelayTimeSamples); + serialAllpass1.setup(maxDelayTimeSamples); + serialAllpass2.setup(maxDelayTimeSamples); + + spreader.setup(spreaderMaxTimeSecond * upRate); reset(); startup(); @@ -179,7 +191,7 @@ void DSPCore::setup(double sampleRate) auto notePitch = calcNotePitch(noteNumber); \ interpPitch.METHOD(notePitch); \ \ - externalInputGain.METHOD(pv[ID::externalInputGain]->getDouble()); \ + externalInputGain.METHOD(pv[ID::externalInputGain]->getDouble() * double(0.5)); \ halfClosedGain.METHOD(pv[ID::halfClosedGain]->getDouble()); \ halfClosedDensity.METHOD(pv[ID::halfClosedDensityHz]->getDouble() / upRate); \ halfClosedHighpassCutoff.METHOD(pv[ID::halfClosedHighpassHz]->getDouble() / upRate); \ @@ -197,16 +209,17 @@ void DSPCore::setup(double sampleRate) lowShelfCutoff.METHOD(EMAFilter::cutoffToP( \ std::min(pv[ID::lowShelfFrequencyHz]->getDouble() / upRate, double(0.5)))); \ lowShelfGain.METHOD(pv[ID::lowShelfGain]->getDouble()); \ - stereoBalance.METHOD(pv[ID::stereoBalance]->getDouble()); \ - stereoMerge.METHOD(pv[ID::stereoMerge]->getDouble() / double(2)); \ + \ + spreaderSplit.METHOD(pv[ID::spreaderSplitHz]->getDouble() / upRate); \ + spreaderSpread.METHOD(pv[ID::spreaderSpread]->getDouble()); \ \ auto gain = pv[ID::outputGain]->getDouble(); \ outputGain.METHOD(gain); \ \ envelopeNoise.setTime(pv[ID::noiseDecaySeconds]->getDouble() * upRate); \ \ - const auto halfCloseDecaySecond = pv[ID::halfCloseDecaySecond]->getDouble(); \ - envelopeHalfClosed.setTime(halfCloseDecaySecond *upRate, closeReleaseSecond *upRate); \ + envelopeHalfClosed.setTime( \ + pv[ID::halfCloseDecaySecond]->getDouble() * upRate, closeReleaseSecond * upRate); \ \ if (!pv[ID::release]->getInt() && noteStack.empty()) { \ envelopeRelease.setTime( \ @@ -217,9 +230,7 @@ void DSPCore::setup(double sampleRate) upRate, pv[ID::closeAttackSeconds]->getDouble(), closeReleaseSecond, \ pv[ID::closeGain]->getDouble()); \ \ - for (auto &x : halfClosedNoise) { \ - x.setDecay(pv[ID::halfClosedPulseSecond]->getDouble() * upRate); \ - } \ + halfClosedNoise.setDecay(pv[ID::halfClosedPulseSecond]->getDouble() * upRate); \ \ updateDelayTime(); @@ -227,6 +238,7 @@ void DSPCore::updateUpRate() { upRate = sampleRate * fold[overSampling]; SmootherCommon::setSampleRate(upRate); + spreader.updateBaseTime(spreaderMaxTimeSecond * upRate); } void DSPCore::updateDelayTime() @@ -245,16 +257,14 @@ void DSPCore::updateDelayTime() const auto t1 = delayTimeBase / double(idx + 1); const auto t2 = delayTimeBase * (circularModes[idx] / circularModes[nAllpass - 1]); const auto harmonics = std::lerp(t1, t2, shape); - serialAllpass1[0].timeInSamples[idx] = harmonics + timeDist(paramRng); - serialAllpass1[1].timeInSamples[idx] = harmonics + timeDist(paramRng); - serialAllpass2[0].timeInSamples[idx] = pitchRatio * (harmonics + timeDist(paramRng)); - serialAllpass2[1].timeInSamples[idx] = pitchRatio * (harmonics + timeDist(paramRng)); + serialAllpass1.timeInSamples[idx] = harmonics + timeDist(paramRng); + serialAllpass2.timeInSamples[idx] = pitchRatio * (harmonics + timeDist(paramRng)); } const size_t nDelay1 = 1 + pv[ID::allpassDelayCount1]->getInt(); const size_t nDelay2 = 1 + pv[ID::allpassDelayCount2]->getInt(); - for (auto &x : serialAllpass1) x.nDelay = nDelay1; - for (auto &x : serialAllpass2) x.nDelay = nDelay2; + serialAllpass1.nDelay = nDelay1; + serialAllpass2.nDelay = nDelay2; } void DSPCore::reset() @@ -269,20 +279,24 @@ void DSPCore::reset() startup(); + halfClosedDensityScaler = double(1); + halfClosedHighpassScaler = double(1); + delayTimeModOffset = 0; + impulse = 0; releaseSmoother.reset(); envelopeNoise.reset(); envelopeHalfClosed.reset(); envelopeRelease.reset(); envelopeClose.reset(); - for (auto &x : halfClosedNoise) x.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(); + halfClosedNoise.reset(); + feedbackBuffer1 = 0; + feedbackBuffer2 = 0; + serialAllpass1.reset(); + serialAllpass2.reset(); + + prevExtIn.fill({}); + halfbandIir.reset(); } void DSPCore::startup() @@ -302,7 +316,7 @@ void DSPCore::setParameters() ASSIGN_PARAMETER(push); } -std::array DSPCore::processFrame(const std::array &externalInput) +double DSPCore::processFrame(const std::array &externalInput) { using ID = ParameterID::ID; const auto &pv = param.value; @@ -310,11 +324,11 @@ std::array DSPCore::processFrame(const std::array &externa const auto envRelease = envelopeRelease.process(); const auto extGain = externalInputGain.process(); - const auto hcGain = halfClosedGain.process() * envelopeHalfClosed.process(); - const auto hcDensity = halfClosedDensity.process(); + const auto hcGain = velocity * halfClosedGain.process() * envelopeHalfClosed.process(); + const auto hcDensity = halfClosedDensityScaler * halfClosedDensity.process(); const auto hcCutoff = halfClosedHighpassCutoff.process(); - auto timeModAmt = delayTimeModAmount.process(); + auto timeModAmt = delayTimeModOffset + delayTimeModAmount.process(); // timeModAmt += (double(1) - envRelease) * (double(10) * upRate / double(48000)); auto apGain1 = allpassFeed1.process(); @@ -328,8 +342,6 @@ std::array DSPCore::processFrame(const std::array &externa 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() * envRelease; std::uniform_real_distribution dist{double(-1), double(1)}; @@ -338,68 +350,40 @@ std::array DSPCore::processFrame(const std::array &externa noiseEnv += envelopeClose.process(); } - std::array excitation{ - -apGain1 * feedbackBuffer1[0], -apGain1 * feedbackBuffer1[1]}; + double excitation = -apGain1 * feedbackBuffer1; if (impulse != 0) { - excitation[0] += impulse; - excitation[1] += impulse; + excitation += impulse; impulse = 0; } else { const auto ipow = [](double v) { return std::copysign(v * v * v, v); }; - excitation[0] += noiseEnv * ipow(dist(noiseRng)); - excitation[1] += noiseEnv * ipow(dist(noiseRng)); - - const auto g = hcGain * velocity; - excitation[0] - += g * halfClosedNoise[0].process(hcDensity, double(1), hcCutoff, noiseRng); - excitation[1] - += g * halfClosedNoise[1].process(hcDensity, double(1), hcCutoff, noiseRng); + excitation += noiseEnv * ipow(dist(noiseRng)); + + excitation += hcGain + * halfClosedNoise.process( + hcDensity, double(1), halfClosedHighpassScaler * hcCutoff, noiseRng); } if (useExternalInput) { - excitation[0] += externalInput[0] * extGain; - excitation[1] += externalInput[1] * extGain; + excitation += (externalInput[0] + externalInput[1]) * extGain; } // 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) + auto ap1Out0 = std::lerp(serialAllpass1.sum(apMixSign), feedbackBuffer1, 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); + feedbackBuffer1 = serialAllpass1.process( + excitation, hsCut, hsGain, lsCut, lsGain, apGain1, pitchRatio, timeModAmt); - auto cymbal0 - = std::lerp(serialAllpass2[0].sum(apMixSign), feedbackBuffer2[0], apMixSpike) + auto cymbal0 = std::lerp(serialAllpass2.sum(apMixSign), feedbackBuffer2, 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, + feedbackBuffer2 = serialAllpass2.process( + ap1Out0 - apGain2 * feedbackBuffer2, hsCut, hsGain, lsCut, lsGain, apGain2, pitchRatio, timeModAmt); - 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), - }; + return outGain * cymbal0; } void DSPCore::process( @@ -413,8 +397,7 @@ void DSPCore::process( SmootherCommon::setBufferSize(double(length)); SmootherCommon::setSampleRate(upRate); - std::array prevExtIn = halfbandInput[0]; - std::array frame{}; + double frame = 0; for (size_t i = 0; i < length; ++i) { processMidiNote(i); @@ -422,27 +405,25 @@ void DSPCore::process( const double extIn1 = in1 == nullptr ? 0 : in1[i]; if (overSampling) { - frame = processFrame({ + const auto sig0 = 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]; + const auto sig1 = processFrame({extIn0, extIn1}); - frame[0] = halfbandIir[0].process(halfbandInput[0]); - frame[1] = halfbandIir[1].process(halfbandInput[1]); - out0[i] = float(frame[0]); - out1[i] = float(frame[1]); + frame = halfbandIir.process({sig0, sig1}); } else { frame = processFrame({extIn0, extIn1}); - out0[i] = float(frame[0]); - out1[i] = float(frame[1]); } + const auto spSplit = spreaderSplit.process(baseSampleRateKp); + const auto spSpread = spreaderSpread.process(baseSampleRateKp); + auto sig = spreader.process(frame, spSplit, spSpread); + + out0[i] = float(sig[0]); + out1[i] = float(sig[1]); + prevExtIn = {extIn0, extIn1}; } } @@ -458,15 +439,17 @@ void DSPCore::noteOn(NoteInfo &info) auto notePitch = calcNotePitch(info.noteNumber); interpPitch.push(notePitch); - velocity = velocityMap.map(info.velocity); + const double velocityLin = info.velocity; + velocity = decibelMap( + double(info.velocity), pv[ID::velocityToOutputGain]->getDouble(), double(0), false); if (pv[ID::resetSeedAtNoteOn]->getInt()) noiseRng.seed(pv[ID::seed]->getInt()); const auto lossGain = !pv[ID::release]->getInt() && noteStack.empty() ? envelopeRelease.value : pv[ID::lossGain]->getDouble(); - for (auto &x : serialAllpass1) x.applyGain(lossGain); - for (auto &x : serialAllpass2) x.applyGain(lossGain); + serialAllpass1.applyGain(lossGain); + serialAllpass2.applyGain(lossGain); releaseSmoother.prepare( double(0.1) * envelopeNoise.process(), @@ -476,7 +459,12 @@ void DSPCore::noteOn(NoteInfo &info) impulse = oscGain; envelopeNoise.trigger(oscGain); - envelopeHalfClosed.trigger(pv[ID::halfCloseSustainLevel]->getDouble()); + halfClosedDensityScaler = std::exp2( + double(4) * velocityLin * pv[ID::velocityToHalfClosedDensity]->getDouble()); + halfClosedHighpassScaler = std::exp2( + double(5) * velocityLin * pv[ID::velocityToHalfClosedHighpass]->getDouble()); + envelopeHalfClosed.trigger( + pv[ID::halfCloseSustainLevel]->getDouble(), halfClosedDensityScaler); envelopeRelease.trigger(double(1)); envelopeRelease.setTime(double(1), true); @@ -485,6 +473,8 @@ void DSPCore::noteOn(NoteInfo &info) upRate, pv[ID::closeAttackSeconds]->getDouble(), closeReleaseSecond, pv[ID::closeGain]->getDouble(), velocity); + delayTimeModOffset = pv[ID::velocityToDelayTimeMod]->getDouble(); + noteStack.push_back(info); } diff --git a/DoubleLoopCymbal/source/dsp/dspcore.hpp b/DoubleLoopCymbal/source/dsp/dspcore.hpp index 219916f7..8bb8ce60 100644 --- a/DoubleLoopCymbal/source/dsp/dspcore.hpp +++ b/DoubleLoopCymbal/source/dsp/dspcore.hpp @@ -97,13 +97,11 @@ class DSPCore { void updateUpRate(); void updateDelayTime(); double calcNotePitch(double note); - std::array processFrame(const std::array &externalInput); + double 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; @@ -129,11 +127,12 @@ class DSPCore { ExpSmoother highShelfGain; ExpSmoother lowShelfCutoff; ExpSmoother lowShelfGain; - ExpSmoother stereoBalance; - ExpSmoother stereoMerge; ExpSmoother outputGain; bool useExternalInput = false; + double halfClosedDensityScaler = double(1); + double halfClosedHighpassScaler = double(1); + double delayTimeModOffset = 0; pcg64 noiseRng{0}; pcg64 paramRng{0}; @@ -143,12 +142,17 @@ class DSPCore { ExpDSREnvelope envelopeHalfClosed; ExpSREnvelope envelopeRelease; ExpADEnvelope envelopeClose; - std::array, 2> halfClosedNoise; - std::array feedbackBuffer1{}; - std::array feedbackBuffer2{}; - std::array, 2> serialAllpass1; - std::array, 2> serialAllpass2; - - std::array, 2> halfbandInput{}; - std::array>, 2> halfbandIir; + HalfClosedNoise halfClosedNoise; + double feedbackBuffer1 = 0; + double feedbackBuffer2 = 0; + SerialAllpass serialAllpass1; + SerialAllpass serialAllpass2; + + double baseSampleRateKp = double(1); + ExpSmootherLocal spreaderSplit; + ExpSmootherLocal spreaderSpread; + Spreader spreader; + + std::array prevExtIn{}; + HalfBandIIR> halfbandIir; }; diff --git a/DoubleLoopCymbal/source/editor.cpp b/DoubleLoopCymbal/source/editor.cpp index 3bc82487..62f45da1 100644 --- a/DoubleLoopCymbal/source/editor.cpp +++ b/DoubleLoopCymbal/source/editor.cpp @@ -133,14 +133,14 @@ bool Editor::prepareUI() addCheckbox( mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, "Release", ID::release); - addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Stereo Balance"); + addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Spread"); 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, + mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::spreaderSpread, Scales::defaultScale, false, 5); + addLabel(mixLeft0, mixTop7, labelWidth, labelHeight, uiTextSize, "Split [Hz]"); + addTextKnob( + mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::spreaderSplitHz, + Scales::cutoffFrequencyHz, false, 5); addToggleButton( mixLeft0, mixTop8, groupLabelWidth, labelHeight, uiTextSize, "External Input", @@ -318,6 +318,43 @@ bool Editor::prepareUI() lowShelfGainTextKnob->lowSensitivity = 1.0 / 30000.0; } + // TODO: Delays. + constexpr auto velocityTop0 = filterTop4 + labelY; + constexpr auto velocityTop1 = velocityTop0 + 1 * labelY; + constexpr auto velocityTop2 = velocityTop0 + 2 * labelY; + constexpr auto velocityTop3 = velocityTop0 + 3 * labelY; + constexpr auto velocityTop4 = velocityTop0 + 4 * labelY; + constexpr auto velocityLeft0 = left8; + constexpr auto velocityLeft1 = velocityLeft0 + labelWidth + 2 * margin; + addGroupLabel( + velocityLeft0, velocityTop0, groupLabelWidth, labelHeight, uiTextSize, + "Velocity Sensitivity"); + + addLabel( + velocityLeft0, velocityTop1, labelWidth, labelHeight, uiTextSize, + "> Output Gain [dB]"); + addTextKnob( + velocityLeft1, velocityTop1, labelWidth, labelHeight, uiTextSize, + ID::velocityToOutputGain, Scales::velocityRangeDecibel, false, 5); + addLabel( + velocityLeft0, velocityTop2, labelWidth, labelHeight, uiTextSize, + "> Half Closed Density"); + addTextKnob( + velocityLeft1, velocityTop2, labelWidth, labelHeight, uiTextSize, + ID::velocityToHalfClosedDensity, Scales::bipolarScale, false, 5); + addLabel( + velocityLeft0, velocityTop3, labelWidth, labelHeight, uiTextSize, + "> Half Closed Highpass"); + addTextKnob( + velocityLeft1, velocityTop3, labelWidth, labelHeight, uiTextSize, + ID::velocityToHalfClosedHighpass, Scales::bipolarScale, false, 5); + addLabel( + velocityLeft0, velocityTop4, labelWidth, labelHeight, uiTextSize, + "> Modulation [sample]"); + addTextKnob( + velocityLeft1, velocityTop4, labelWidth, labelHeight, uiTextSize, + ID::velocityToDelayTimeMod, Scales::delayTimeModAmount, false, 5); + // TODO: Delays. constexpr auto thirdTop0 = top0 + 0 * labelY; constexpr auto thirdTop1 = thirdTop0 + 1 * labelY; @@ -334,6 +371,9 @@ bool Editor::prepareUI() constexpr auto thirdTop12 = thirdTop0 + 12 * labelY; constexpr auto thirdTop13 = thirdTop0 + 13 * labelY; constexpr auto thirdTop14 = thirdTop0 + 14 * labelY; + constexpr auto thirdTop15 = thirdTop0 + 15 * labelY; + constexpr auto thirdTop16 = thirdTop0 + 16 * labelY; + constexpr auto thirdTop17 = thirdTop0 + 17 * labelY; constexpr auto thirdLeft0 = left12; constexpr auto thirdLeft1 = thirdLeft0 + labelWidth + 2 * margin; addGroupLabel( diff --git a/DoubleLoopCymbal/source/parameter.cpp b/DoubleLoopCymbal/source/parameter.cpp index 2867cc7e..635539ad 100644 --- a/DoubleLoopCymbal/source/parameter.cpp +++ b/DoubleLoopCymbal/source/parameter.cpp @@ -50,5 +50,7 @@ UIntScale Scales::allpassDelayCount(nAllpass - 1); DecibelScale Scales::cutoffFrequencyHz(0, 100, false); DecibelScale Scales::shelvingGain(-60, 0, true); +LinearScale Scales::velocityRangeDecibel(-100.0, 0.0); + } // namespace Synth } // namespace Steinberg diff --git a/DoubleLoopCymbal/source/parameter.hpp b/DoubleLoopCymbal/source/parameter.hpp index 23b69147..c80fd2a9 100644 --- a/DoubleLoopCymbal/source/parameter.hpp +++ b/DoubleLoopCymbal/source/parameter.hpp @@ -48,8 +48,8 @@ enum ID { resetSeedAtNoteOn, release, - stereoBalance, - stereoMerge, + spreaderSpread, + spreaderSplitHz, useExternalInput, externalInputGain, @@ -92,6 +92,12 @@ enum ID { lowShelfFrequencyHz, lowShelfGain, + // useNoteOffVelocityForClose, // TODO + velocityToOutputGain, + velocityToHalfClosedDensity, + velocityToHalfClosedHighpass, + velocityToDelayTimeMod, + reservedParameter0, reservedGuiParameter0 = reservedParameter0 + nReservedParameter, @@ -121,6 +127,8 @@ struct Scales { static SomeDSP::DecibelScale cutoffFrequencyHz; static SomeDSP::DecibelScale shelvingGain; + + static SomeDSP::LinearScale velocityRangeDecibel; }; struct GlobalParameter : public ParameterInterface { @@ -149,12 +157,12 @@ struct GlobalParameter : public ParameterInterface { value[ID::release] = std::make_unique(0, Scales::boolScale, "release", 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", + value[ID::spreaderSpread] = std::make_unique( + Scales::defaultScale.invmap(0.5), Scales::defaultScale, "spreaderSpread", Info::kCanAutomate); + value[ID::spreaderSplitHz] = std::make_unique( + Scales::cutoffFrequencyHz.invmap(500.0), Scales::cutoffFrequencyHz, + "spreaderSplitHz", Info::kCanAutomate); value[ID::useExternalInput] = std::make_unique( 0, Scales::boolScale, "useExternalInput", Info::kCanAutomate); @@ -263,6 +271,19 @@ struct GlobalParameter : public ParameterInterface { Scales::shelvingGain.invmapDB(-1.0), Scales::shelvingGain, "lowShelfGain", Info::kCanAutomate); + value[ID::velocityToOutputGain] = std::make_unique( + Scales::velocityRangeDecibel.invmap(-60.0), Scales::velocityRangeDecibel, + "velocityToOutputGain", Info::kCanAutomate); + value[ID::velocityToHalfClosedDensity] = std::make_unique( + Scales::bipolarScale.invmap(0.5), Scales::bipolarScale, + "velocityToHalfClosedDensity", Info::kCanAutomate); + value[ID::velocityToHalfClosedHighpass] = std::make_unique( + Scales::bipolarScale.invmap(0.4), Scales::bipolarScale, + "velocityToHalfClosedHighpass", Info::kCanAutomate); + value[ID::velocityToDelayTimeMod] = std::make_unique( + Scales::delayTimeModAmount.invmap(0.5), Scales::delayTimeModAmount, + "velocityToDelayTimeMod", Info::kCanAutomate); + for (size_t idx = 0; idx < nReservedParameter; ++idx) { auto indexStr = std::to_string(idx); value[ID::reservedParameter0 + idx] = std::make_unique(