From 14ddef7b1d9cba789cccbd96a6bf08d6842c9926 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Fri, 19 Jul 2024 13:20:24 +0900 Subject: [PATCH] Update GlitchSprinkler (WIP) --- GlitchSprinkler/source/dsp/dspcore.cpp | 158 ++++++++++++++---- GlitchSprinkler/source/dsp/dspcore.hpp | 28 ++-- GlitchSprinkler/source/dsp/polynomial.hpp | 52 ++++++ GlitchSprinkler/source/dsp/tuning.hpp | 4 +- GlitchSprinkler/source/editor.cpp | 136 ++++++++++++--- .../source/gui/polynomialxypad.hpp | 3 + .../source/gui/randomizebutton.hpp | 11 ++ GlitchSprinkler/source/parameter.cpp | 8 +- GlitchSprinkler/source/parameter.hpp | 71 +++++++- common/gui/knob.hpp | 9 +- 10 files changed, 400 insertions(+), 80 deletions(-) diff --git a/GlitchSprinkler/source/dsp/dspcore.cpp b/GlitchSprinkler/source/dsp/dspcore.cpp index bfbd6516..3f401f3a 100644 --- a/GlitchSprinkler/source/dsp/dspcore.cpp +++ b/GlitchSprinkler/source/dsp/dspcore.cpp @@ -37,6 +37,8 @@ void DSPCore::setup(double sampleRate_) SmootherCommon::setTime(double(0.2)); + lowpassInterpRate = sampleRate / double(48000 * 64); + reset(); startup(); } @@ -94,9 +96,9 @@ void Voice::reset() noteVelocity = 0; unisonIndex = 1; + unisonRatio = double(0); unisonPan = double(0.5); - unisonPanNext = double(0.5); - unisonCentNext = double(0); + unisonGain = double(1); rngArpeggio.seed(pv[ID::seed]->getInt() + unisonIndex); @@ -110,9 +112,19 @@ void Voice::reset() oscSync = double(1); fmIndex = double(0); saturationGain = double(1); - decayGain = double(0); decayRatio = double(1); + decayGain = double(0); polynomialCoefficients.fill({}); + + filterDecayRatio = double(1); + filterDecayGain = double(0); + cutoffBase = double(1); + cutoffMod = double(0); + resonanceBase = double(0); + resonanceMod = double(0); + notchBase = double(1); + notchMod = double(0); + lowpass.reset(); } void DSPCore::reset() @@ -199,6 +211,15 @@ std::array Voice::processFrame() lastGain = decayGain * noteVelocity; sig *= lastGain; + // Lowpass. + if (pv[ID::filterSwitch]->getInt()) { + sig = lowpass.process( + sig, core.lowpassInterpRate, cutoffBase + cutoffMod * filterDecayRatio, + resonanceBase + resonanceMod * filterDecayRatio, + notchBase + notchMod * filterDecayRatio); + filterDecayRatio *= filterDecayGain; + } + if (++phaseCounter >= phasePeriod) { phaseCounter = 0; if (scheduleUpdateNote) { @@ -272,9 +293,6 @@ void DSPCore::noteOn(NoteInfo &info) std::iota(noteIndices.begin(), noteIndices.end(), size_t(0)); } - const auto unisonDetuneCent = pv[ID::unisonDetuneCent]->getDouble(); - const auto unisonPanSpread = pv[ID::unisonPanSpread]->getDouble(); - const auto unisonPanOffset = (double(1) - unisonPanSpread) / double(2); for (unsigned idx = 0; idx < noteIndices.size(); ++idx) { auto &vc = voices[noteIndices[idx]]; @@ -286,27 +304,28 @@ void DSPCore::noteOn(NoteInfo &info) } else { vc.noteId = -1; } - vc.unisonGain = double(1) / double(nUnison); - vc.state = Voice::State::active; - vc.unisonIndex = idx + 1; - vc.rngArpeggio.seed(seed + vc.unisonIndex); if (nUnison <= 1) { - vc.unisonPanNext = double(0.5); - vc.unisonCentNext = double(0); + vc.unisonIndex = 0; + vc.unisonRatio = double(0); } else { - const double ratio = double(idx) / double(nUnison - 1); - vc.unisonPanNext = ratio * unisonPanSpread + unisonPanOffset; - vc.unisonCentNext = ratio * unisonDetuneCent; + auto scatterArp = pv[ID::unisonScatterArpeggio]->getInt(); + vc.unisonIndex = scatterArp ? idx + 1 : 1; + vc.unisonRatio = double(idx) / double(nUnison - 1); } + vc.unisonGain = double(1) / double(nUnison); + vc.rngArpeggio.seed(pv[ID::seed]->getInt() + vc.unisonIndex); + if (vc.state == Voice::State::rest && vc.phaseCounter == 0) { vc.arpeggioTimer = 0; vc.arpeggioLoopCounter = 0; + vc.lowpass.reset(); vc.updateNote(); } else if (!param.value[ParameterID::arpeggioSwitch]->getInt()) { vc.scheduleUpdateNote = true; } + vc.state = Voice::State::active; } } @@ -367,19 +386,27 @@ void Voice::updateNote() noteCent = note.cent; } - unisonPan = unisonPanNext; + if (unisonIndex <= 0) { + unisonPan = double(0.5); + } else { + const auto unisonPanSpread = pv[ID::unisonPanSpread]->getDouble(); + const auto unisonPanOffset = (double(1) - unisonPanSpread) / double(2); + unisonPan = unisonRatio * unisonPanSpread + unisonPanOffset; + } // Pitch & phase. const auto tuning = static_cast(pv[ID::tuning]->getInt()); - auto transposeOctave = int(pv[ID::transposeOctave]->getInt()) - transposeOctaveOffset; - auto transposeSemitone + const auto transposeOctave + = int(pv[ID::transposeOctave]->getInt()) - transposeOctaveOffset; + const auto transposeSemitone = int(pv[ID::transposeSemitone]->getInt()) - transposeSemitoneOffset; - auto transposeCent = pv[ID::transposeCent]->getDouble(); + const auto transposeCent = pv[ID::transposeCent]->getDouble(); + const auto unisonDetuneCent = unisonRatio * pv[ID::unisonDetuneCent]->getDouble(); auto getNaturalFreq = [&]() { auto transposeRatio = getPitchRatio( transposeSemitone + noteNumber - 69, transposeOctave, - transposeCent + noteCent + unisonCentNext, tuning); + transposeCent + noteCent + unisonDetuneCent, tuning); return core.pitchModifier * transposeRatio * double(440); }; auto getDiscreteFreq = [&]() { @@ -412,18 +439,57 @@ void Voice::updateNote() auto arpRatio = std::exp2(double(octaveDist(rngArpeggio)) + centDist(rngArpeggio) / double(1200)); - const auto arpeggioScale = pv[ID::arpeggioScale]->getInt(); - if (arpeggioScale == PitchScale::octave) { - // Do nothing. - } else if (arpeggioScale == PitchScale::et5Chromatic) { - arpRatio *= getRandomPitch(rngArpeggio, scaleEt5, tuningRatioEt5); - } else if (arpeggioScale == PitchScale::et12Major) { - arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchC, tuningRatioEt12); - } else if (arpeggioScale == PitchScale::et12Minor) { - arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchA, tuningRatioEt12); - } else if (arpeggioScale == PitchScale::overtone32) { - std::uniform_int_distribution dist{1, 32}; - arpRatio *= double(dist(rngArpeggio)); + if (arpeggioTimer != 0 || arpeggioLoopCounter != 0) { + const auto arpeggioScale = pv[ID::arpeggioScale]->getInt(); + if (arpeggioScale == PitchScale::et5Chromatic) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt5, tuningRatioEt5); + } else if (arpeggioScale == PitchScale::et12ChurchC) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchC, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchD) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchD, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchE) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchE, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchF) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchF, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchG) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchG, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchA) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchA, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12ChurchB) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12ChurchB, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12Sus2) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12Sus2, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12Sus4) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12Sus4, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12Maj7) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12Maj7, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12Min7) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12Min7, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12MajExtended) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12MajExtended, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12MinExtended) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12MinExtended, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12WholeTone2) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12WholeTone2, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12WholeTone3) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12WholeTone3, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12WholeTone4) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12WholeTone4, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::et12Blues) { + arpRatio *= getRandomPitch(rngArpeggio, scaleEt12Blues, tuningRatioEt12); + } else if (arpeggioScale == PitchScale::overtone32) { + std::uniform_int_distribution dist{1, 32}; + arpRatio *= double(dist(rngArpeggio)); + } else if (arpeggioScale == PitchScale::overtone42Melody) { + constexpr std::array melody{2, 8, 10, 32, 34, 40, 42}; + std::uniform_int_distribution dist{0, melody.size() - 1}; + arpRatio *= melody[dist(rngArpeggio)]; + } else if (arpeggioScale == PitchScale::overtoneOdd16) { + constexpr std::array melody{1, 3, 5, 7, 9, 11, 13, 15}; + std::uniform_int_distribution dist{0, melody.size() - 1}; + arpRatio *= melody[dist(rngArpeggio)]; + } + // Defaults to `arpeggioScale == PitchScale::octave`. } freqHz *= arpRatio; @@ -431,6 +497,8 @@ void Voice::updateNote() auto durationVar = pv[ID::arpeggioDurationVariation]->getInt() + 1; std::uniform_int_distribution durVarDist{1, durationVar}; arpeggioTie = durVarDist(rngArpeggio); + } else { + arpeggioTie = 1; } freqHz = std::min(double(0.5) * core.sampleRate, freqHz); @@ -452,8 +520,9 @@ void Voice::updateNote() saturationGain = pv[ID::saturationGain]->getDouble(); // Envelope. - const auto decaySeconds = pv[ID::decaySeconds]->getDouble(); - decayRatio = std::pow(double(1e-3), double(1) / (decaySeconds * core.sampleRate)); + const auto decayTargetGain = pv[ID::decayTargetGain]->getDouble(); + decayRatio + = std::pow(decayTargetGain, double(1) / (arpeggioTie * core.arpeggioDuration)); const auto restChance = pv[ID::arpeggioRestChance]->getDouble(); std::uniform_real_distribution restDist{double(0), double(1)}; @@ -461,6 +530,27 @@ void Voice::updateNote() ? double(0) : unisonGain / decayRatio; lastGain = decayGain; + + // Filter. + if (pv[ID::filterSwitch]->getInt()) { + const auto cutoffBaseOctave = pv[ID::filterCutoffBaseOctave]->getDouble(); + const auto cutoffModOctave = pv[ID::filterCutoffBaseOctave]->getDouble(); + cutoffBase = std::clamp( + freqHz / core.sampleRate * std::exp2(cutoffBaseOctave), double(0), double(0.4999)); + cutoffMod = cutoffModOctave <= Scales::filterCutoffModOctave.getMin() + ? double(0) + : std::min(cutoffBase * std::exp2(cutoffModOctave), double(0.4999) - cutoffBase); + resonanceBase = pv[ID::filterResonanceBase]->getDouble(); + resonanceMod = pv[ID::filterResonanceMod]->getDouble() * (double(1) - resonanceBase); + notchBase = std::exp2(pv[ID::filterNotchBaseOctave]->getDouble()); + const auto notchModOctave = std::exp2(pv[ID::filterNotchModOctave]->getDouble()); + notchMod = notchModOctave <= Scales::filterNotchModOctave.getMin() + ? double(0) + : std::exp2(notchModOctave); + filterDecayRatio = double(1); + filterDecayGain + = std::pow(decayRatio, std::exp2(-pv[ID::filterDecayRatio]->getDouble())); + } } void DSPCore::noteOff(int_fast32_t noteId) diff --git a/GlitchSprinkler/source/dsp/dspcore.hpp b/GlitchSprinkler/source/dsp/dspcore.hpp index ec1af8ad..db83a52b 100644 --- a/GlitchSprinkler/source/dsp/dspcore.hpp +++ b/GlitchSprinkler/source/dsp/dspcore.hpp @@ -58,6 +58,11 @@ class Voice { double noteVelocity = 0; double notePan = 0.5; + unsigned unisonIndex = 1; + double unisonRatio = double(0); + double unisonPan = double(0.5); + double unisonGain = double(1); + std::minstd_rand rngArpeggio{0}; uint_fast32_t arpeggioTie = 1; @@ -67,20 +72,23 @@ class Voice { bool scheduleUpdateNote = false; uint_fast32_t phasePeriod = 0; uint_fast32_t phaseCounter = 0; - - unsigned unisonIndex = 1; - double unisonGain = double(1); - double unisonPan = double(0.5); - double unisonPanNext = double(0.5); - double unisonCentNext = double(0); - double oscSync = double(1); double fmIndex = double(0); double saturationGain = double(1); - double decayGain = double(0); double decayRatio = double(1); + double decayGain = double(0); std::array polynomialCoefficients{}; + double filterDecayRatio = double(1); + double filterDecayGain = double(0); + double cutoffBase = double(1); + double cutoffMod = double(0); + double resonanceBase = double(0); + double resonanceMod = double(0); + double notchBase = double(1); + double notchMod = double(0); + ResonantEmaLowpass1A1 lowpass; + void reset(); void setParameters(); std::array processFrame(); @@ -92,7 +100,7 @@ class DSPCore { public: GlobalParameter param; - double sampleRate = 44100.0; + double sampleRate = 48000.0; bool isPlaying = false; double tempo = 120.0; @@ -101,7 +109,6 @@ class DSPCore { double timeSigUpper = 1.0; double timeSigLower = 4.0; - unsigned seed = 0; double currentBeat = 0; double pitchModifier = double(1); bool isPolyphonic = true; @@ -110,6 +117,7 @@ class DSPCore { uint_fast32_t arpeggioDuration = std::numeric_limits::max(); uint_fast32_t arpeggioLoopLength = std::numeric_limits::max(); + double lowpassInterpRate = double(1) / double(64); DecibelScale velocityMap{-60, 0, true}; ExpSmoother outputGain; diff --git a/GlitchSprinkler/source/dsp/polynomial.hpp b/GlitchSprinkler/source/dsp/polynomial.hpp index e411c53e..f228cc11 100644 --- a/GlitchSprinkler/source/dsp/polynomial.hpp +++ b/GlitchSprinkler/source/dsp/polynomial.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace SomeDSP { @@ -218,4 +219,55 @@ template class PolynomialCoefficientSolve } }; +template class ResonantEmaLowpass1A1 { +private: + Sample cutValue = Sample(1); + Sample apsValue = Sample(1); + Sample resValue = Sample(0); + + Sample v1 = 0; + Sample u1 = 0; + Sample u2 = 0; + +public: + void reset() + { + cutValue = 0; + resValue = 0; + v1 = 0; + u1 = 0; + u2 = 0; + } + + Sample process( + Sample input, + Sample interpRate, + Sample cutoffNormalized, + Sample resonance, + Sample apScale) + { + cutValue += interpRate * (cutoffNormalized - cutValue); + + constexpr Sample pi = std::numbers::pi_v; + const auto freq = pi * std::clamp(cutValue, Sample(0), Sample(0.4999)); + + const auto s = 1 - std::cos(Sample(2) * freq); + const auto c1 = std::sqrt(s * s + Sample(2) * s) - s; + + apsValue += interpRate * (apScale - apsValue); + const auto t = std::tan(std::clamp(apsValue * freq, Sample(0), Sample(0.4999))); + const auto c2 = (t - 1) / (t + 1); + + resValue += interpRate * (resonance - resValue); + const auto q = resValue * (c2 - c1 * c2 + Sample(1)); + + // Bilinear allpass, order 1. + v1 = c2 * (u1 - v1) + u2; + u2 = u1; + + // Exponential moving average (EMA) lowpass. + return u1 += c1 * (input - u1) - q * v1; + } +}; + } // namespace SomeDSP diff --git a/GlitchSprinkler/source/dsp/tuning.hpp b/GlitchSprinkler/source/dsp/tuning.hpp index 68693568..244f9268 100644 --- a/GlitchSprinkler/source/dsp/tuning.hpp +++ b/GlitchSprinkler/source/dsp/tuning.hpp @@ -45,8 +45,8 @@ std::array scaleEt12Sus4{0, 5, 7}; std::array scaleEt12Chord2_4_7_9_11{0, 2, 4, 7, 9, 11}; std::array scaleEt12Maj7{0, 4, 7, 11}; std::array scaleEt12Min7{0, 3, 7, 10}; -std::array scaleEt12MajHillege{0, 4, 7, 11, 14, 18, 21, 25}; -std::array scaleEt12MinHillege{0, 3, 7, 10, 14, 17, 21, 24}; +std::array scaleEt12MajExtended{0, 4, 7, 11, 14, 18, 21, 25}; +std::array scaleEt12MinExtended{0, 3, 7, 10, 14, 17, 21, 24}; std::array scaleEt12WholeTone2{0, 2, 4, 6, 8, 10}; std::array scaleEt12WholeTone3{0, 3, 6, 9}; std::array scaleEt12WholeTone4{0, 4, 8}; diff --git a/GlitchSprinkler/source/editor.cpp b/GlitchSprinkler/source/editor.cpp index 25e190b0..02ce7934 100644 --- a/GlitchSprinkler/source/editor.cpp +++ b/GlitchSprinkler/source/editor.cpp @@ -49,7 +49,7 @@ 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); + = int_least32_t(2 * uiMargin + 19 * labelY - 2 * margin); namespace Steinberg { namespace Vst { @@ -102,17 +102,17 @@ bool Editor::prepareUI() constexpr auto mixLeft0 = left0; constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin; addGroupLabel( - mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Mix & Options"); + mixLeft0, mixTop0, groupLabelWidth, labelHeight, uiTextSize, "Oscillator"); addLabel(mixLeft0, mixTop1, labelWidth, labelHeight, uiTextSize, "Output [dB]"); addTextKnob( mixLeft1, mixTop1, labelWidth, labelHeight, uiTextSize, ID::outputGain, Scales::gain, true, 5); - addLabel(mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "Decay [s]"); + addLabel(mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "Decay to [dB]"); addTextKnob( - mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, ID::decaySeconds, - Scales::decaySeconds, false, 5); + mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, ID::decayTargetGain, + Scales::decayTargetGain, true, 5); addLabel(mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Osc. Sync."); addTextKnob( mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, ID::oscSync, @@ -166,6 +166,58 @@ bool Editor::prepareUI() addCheckbox( mixLeft1, mixTop12, labelWidth, labelHeight, uiTextSize, "Release", ID::release); + // Filter. + constexpr auto filterLabelWidth = 100.0f; + constexpr auto filterTop0 = mixTop12 + labelY; + constexpr auto filterTop1 = filterTop0 + 1 * labelY; + constexpr auto filterTop2 = filterTop0 + 2 * labelY; + constexpr auto filterTop3 = filterTop0 + 3 * labelY; + constexpr auto filterTop4 = filterTop0 + 4 * labelY; + constexpr auto filterTop5 = filterTop0 + 5 * labelY; + constexpr auto filterLeft0 = left0; + constexpr auto filterLeft1 = filterLeft0 + 1 * filterLabelWidth + 3 * margin; + constexpr auto filterLeft2 = filterLeft0 + 2 * filterLabelWidth + 6 * margin; + addToggleButton( + filterLeft0, filterTop0, groupLabelWidth, labelHeight, uiTextSize, "Filter", + ID::filterSwitch); + + addLabel( + filterLeft0, filterTop1, labelWidth, labelHeight, uiTextSize, "Decay Time Ratio"); + addTextKnob( + filterLeft0 + labelWidth + 2 * margin, filterTop1, labelWidth, labelHeight, + uiTextSize, ID::filterDecayRatio, Scales::filterDecayRatio, false, 5); + + addLabel(filterLeft1, filterTop2, filterLabelWidth, labelHeight, uiTextSize, "Base"); + addLabel( + filterLeft2, filterTop2, filterLabelWidth, labelHeight, uiTextSize, "Env. Mod"); + + addLabel( + filterLeft0, filterTop3, filterLabelWidth, labelHeight, uiTextSize, "Cutoff [oct.]"); + addTextKnob( + filterLeft1, filterTop3, filterLabelWidth, labelHeight, uiTextSize, + ID::filterCutoffBaseOctave, Scales::filterCutoffBaseOctave, false, 5); + addTextKnob( + filterLeft2, filterTop3, filterLabelWidth, labelHeight, uiTextSize, + ID::filterCutoffModOctave, Scales::filterCutoffModOctave, false, 5); + + addLabel( + filterLeft0, filterTop4, filterLabelWidth, labelHeight, uiTextSize, "Resonance"); + addTextKnob( + filterLeft1, filterTop4, filterLabelWidth, labelHeight, uiTextSize, + ID::filterResonanceBase, Scales::defaultScale, false, 5); + addTextKnob( + filterLeft2, filterTop4, filterLabelWidth, labelHeight, uiTextSize, + ID::filterResonanceMod, Scales::defaultScale, false, 5); + + addLabel( + filterLeft0, filterTop5, filterLabelWidth, labelHeight, uiTextSize, "Notch [oct.]"); + addTextKnob( + filterLeft1, filterTop5, filterLabelWidth, labelHeight, uiTextSize, + ID::filterNotchBaseOctave, Scales::filterNotchBaseOctave, false, 5); + addTextKnob( + filterLeft2, filterTop5, filterLabelWidth, labelHeight, uiTextSize, + ID::filterNotchModOctave, Scales::filterNotchModOctave, false, 5); + // Waveform. constexpr auto waveformTop0 = top0 + 0 * labelY; constexpr auto waveformTop1 = waveformTop0 + 1 * labelY; @@ -267,14 +319,38 @@ bool Editor::prepareUI() addOptionMenu( arpLeft1, arpTop8, labelWidth, labelHeight, uiTextSize, ID::arpeggioScale, { - "Octave", "ET 5 Chromatic", "ET 12 Major", "ET 12 Minor", - "Overtone 32", "- Reserved 05 -", "- Reserved 06 -", "- Reserved 07 -", - "- Reserved 08 -", "- Reserved 09 -", "- Reserved 10 -", "- Reserved 11 -", - "- Reserved 12 -", "- Reserved 13 -", "- Reserved 14 -", "- Reserved 15 -", - "- Reserved 16 -", "- Reserved 17 -", "- Reserved 18 -", "- Reserved 19 -", - "- Reserved 20 -", "- Reserved 21 -", "- Reserved 22 -", "- Reserved 23 -", - "- Reserved 24 -", "- Reserved 25 -", "- Reserved 26 -", "- Reserved 27 -", - "- Reserved 28 -", "- Reserved 29 -", "- Reserved 30 -", "- Reserved 31 -", + "Octave", + "ET 5 Chromatic", + "ET 12 Church C (Major Scale)", + "ET 12 Church D", + "ET 12 Church E", + "ET 12 Church F", + "ET 12 Church G", + "ET 12 Church A (Minor Scale)", + "ET 12 Church B", + "ET 12 Suspended 2", + "ET 12 Suspended 4", + "ET 12 Major 7", + "ET 12 Minor 7", + "ET 12 Major 7 Extended", + "ET 12 Minor 7 Extended", + "ET 12 Whole Tone 2", + "ET 12 Whole Tone 3", + "ET 12 Whole Tone 4", + "ET 12 Blues", + "Overtone 32", + "The 42 Melody", + "Overtone Odd 16", + "- Reserved 22 -", + "- Reserved 23 -", + "- Reserved 24 -", + "- Reserved 25 -", + "- Reserved 26 -", + "- Reserved 27 -", + "- Reserved 28 -", + "- Reserved 29 -", + "- Reserved 30 -", + "- Reserved 31 -", }); addLabel(arpLeft0, arpTop9, labelWidth, labelHeight, uiTextSize, "Pitch Drift [cent]"); addTextKnob( @@ -295,6 +371,7 @@ bool Editor::prepareUI() constexpr auto unisonTop1 = unisonTop0 + 1 * labelY; constexpr auto unisonTop2 = unisonTop0 + 2 * labelY; constexpr auto unisonTop3 = unisonTop0 + 3 * labelY; + constexpr auto unisonTop4 = unisonTop0 + 4 * labelY; constexpr auto unisonLeft0 = left8; constexpr auto unisonLeft1 = unisonLeft0 + labelWidth + 2 * margin; addGroupLabel( @@ -305,33 +382,40 @@ bool Editor::prepareUI() unisonLeft1, unisonTop1, labelWidth, labelHeight, uiTextSize, ID::unisonVoice, Scales::unisonVoice, false, 0, 1); addLabel(unisonLeft0, unisonTop2, labelWidth, labelHeight, uiTextSize, "Detune [cent]"); - addTextKnob( + auto unisonDetuneCentTextKnob = addTextKnob( unisonLeft1, unisonTop2, labelWidth, labelHeight, uiTextSize, ID::unisonDetuneCent, Scales::unisonDetuneCent, false, 5); + if (unisonDetuneCentTextKnob) { + unisonDetuneCentTextKnob->sensitivity = 1.0 / 1200.0; + unisonDetuneCentTextKnob->lowSensitivity = 1.0 / 120000.0; + } addLabel(unisonLeft0, unisonTop3, labelWidth, labelHeight, uiTextSize, "Pan Spread"); addTextKnob( unisonLeft1, unisonTop3, labelWidth, labelHeight, uiTextSize, ID::unisonPanSpread, Scales::defaultScale, 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); + addCheckbox( + unisonLeft0, unisonTop4, labelWidth, labelHeight, uiTextSize, "Scatter Arpeggio", + ID::unisonScatterArpeggio); // Plugin name. constexpr auto splashMargin = uiMargin; - constexpr auto splashTop = top0 + 18 * labelY; - constexpr auto splashLeft = left0; + constexpr auto splashTop = top0 + 17 * labelY; + constexpr auto splashLeft = left4; addSplashScreen( splashLeft, splashTop, labelWidth, splashHeight, splashMargin, splashMargin, defaultWidth - 2 * splashMargin, defaultHeight - 2 * splashMargin, pluginNameTextSize, "GlitchSprinkler", false); + // Randomize button. + const auto randomButtonTop = splashTop; + const auto randomButtonLeft = splashLeft + labelWidth + 2 * margin; + auto panicButton = new RandomizeButton( + CRect( + randomButtonLeft, randomButtonTop, randomButtonLeft + labelWidth, + randomButtonTop + splashHeight), + this, 0, "Randomize", getFont(pluginNameTextSize), palette, this); + frame->addView(panicButton); + return true; } diff --git a/GlitchSprinkler/source/gui/polynomialxypad.hpp b/GlitchSprinkler/source/gui/polynomialxypad.hpp index 6ccbe2e2..dd01320a 100644 --- a/GlitchSprinkler/source/gui/polynomialxypad.hpp +++ b/GlitchSprinkler/source/gui/polynomialxypad.hpp @@ -356,6 +356,9 @@ class PolynomialXYPad : public ArrayControl { [&](double ratio) { return double(0.5) + double(0.5) * std::tanh(std::sin(twopi * ratio)); }); + } else if (event.character == '0') { // Preset: saturated sine. + setControlPoints( + [&](double ratio) { return ratio; }, [&](double ratio) { return double(0.5); }); } else if (event.character == 'r') { // Randomize. setControlPoints( [&](double ratio) { return ratio; }, diff --git a/GlitchSprinkler/source/gui/randomizebutton.hpp b/GlitchSprinkler/source/gui/randomizebutton.hpp index 913663e2..02ed3547 100644 --- a/GlitchSprinkler/source/gui/randomizebutton.hpp +++ b/GlitchSprinkler/source/gui/randomizebutton.hpp @@ -119,7 +119,18 @@ class RandomizeButton : public CControl { Rng rng(seeds); std::uniform_real_distribution uniform{0.0, 1.0}; + setParam(ID::fmIndex, uniform(rng)); + setParam( + ID::saturationGain, + std::clamp( + (double(10) + double(6) * uniform(rng)) / double(16), double(0), double(1))); + setParam(ID::randomizeFmIndex, uniform(rng)); + for (size_t i = 0; i < nPolyOscControl; ++i) { + setParam(ID::polynomialPointX0 + i, double(i + 1) / double(nPolyOscControl + 2)); + setParam(ID::polynomialPointY0 + i, uniform(rng)); + } setParam(ID::seed, uniform(rng)); + setParam(ID::arpeggioOctave, uniform(rng)); } invalid(); diff --git a/GlitchSprinkler/source/parameter.cpp b/GlitchSprinkler/source/parameter.cpp index 13175bc0..e7f655e3 100644 --- a/GlitchSprinkler/source/parameter.cpp +++ b/GlitchSprinkler/source/parameter.cpp @@ -36,10 +36,16 @@ UIntScale Scales::seed(1 << 23); DecibelScale Scales::gain(-100.0, 60.0, true); -DecibelScale Scales::decaySeconds(-100.0, 40.0, false); +DecibelScale Scales::decayTargetGain(-150.0, 0.0, false); DecibelScale Scales::fmIndex(-60.0, 40.0, true); LinearScale Scales::randomizeFmIndex(0.0, 4.0); +LinearScale Scales::filterDecayRatio(-10.0, 10.0); +LinearScale Scales::filterCutoffBaseOctave(0.0, 10.0); +LinearScale Scales::filterCutoffModOctave(-10, 10); +LinearScale Scales::filterNotchBaseOctave(-4, 4); +LinearScale Scales::filterNotchModOctave(-4, 4); + LinearScale Scales::polynomialPointY(-0.5, 0.5); UIntScale Scales::transposeOctave(2 * transposeOctaveOffset); diff --git a/GlitchSprinkler/source/parameter.hpp b/GlitchSprinkler/source/parameter.hpp index 4a1844b3..e211e7f6 100644 --- a/GlitchSprinkler/source/parameter.hpp +++ b/GlitchSprinkler/source/parameter.hpp @@ -61,9 +61,27 @@ enum Tuning : uint32_t { enum PitchScale { octave, et5Chromatic, - et12Major, - et12Minor, + et12ChurchC, // Major + et12ChurchD, + et12ChurchE, + et12ChurchF, + et12ChurchG, + et12ChurchA, // Minor + et12ChurchB, + et12Sus2, + et12Sus4, + et12Maj7, + et12Min7, + et12MajExtended, + et12MinExtended, + et12WholeTone2, + et12WholeTone3, + et12WholeTone4, + et12Blues, + overtone32, + overtone42Melody, + overtoneOdd16, PitchScale_ENUM_LENGTH, }; @@ -74,7 +92,7 @@ enum ID { outputGain, - decaySeconds, + decayTargetGain, oscSync, fmIndex, saturationGain, @@ -89,6 +107,15 @@ enum ID { polyphonic, release, + filterSwitch, + filterDecayRatio, + filterCutoffBaseOctave, + filterCutoffModOctave, + filterResonanceBase, + filterResonanceMod, + filterNotchBaseOctave, + filterNotchModOctave, + polynomialPointX0, polynomialPointY0 = polynomialPointX0 + nPolyOscControl, @@ -105,6 +132,7 @@ enum ID { unisonVoice, unisonDetuneCent, unisonPanSpread, + unisonScatterArpeggio, reservedParameter0, reservedGuiParameter0 = reservedParameter0 + nReservedParameter, @@ -122,10 +150,16 @@ struct Scales { static SomeDSP::DecibelScale gain; - static SomeDSP::DecibelScale decaySeconds; + static SomeDSP::DecibelScale decayTargetGain; static SomeDSP::DecibelScale fmIndex; static SomeDSP::LinearScale randomizeFmIndex; + static SomeDSP::LinearScale filterDecayRatio; + static SomeDSP::LinearScale filterCutoffBaseOctave; + static SomeDSP::LinearScale filterCutoffModOctave; + static SomeDSP::LinearScale filterNotchBaseOctave; + static SomeDSP::LinearScale filterNotchModOctave; + static SomeDSP::LinearScale polynomialPointY; static SomeDSP::UIntScale transposeOctave; @@ -163,8 +197,8 @@ struct GlobalParameter : public ParameterInterface { value[ID::outputGain] = std::make_unique( Scales::gain.invmap(1.0), Scales::gain, "outputGain", Info::kCanAutomate); - value[ID::decaySeconds] = std::make_unique( - Scales::decaySeconds.invmap(1.0), Scales::decaySeconds, "decaySeconds", + value[ID::decayTargetGain] = std::make_unique( + Scales::decayTargetGain.invmapDB(-1.0), Scales::decayTargetGain, "decayTargetGain", Info::kCanAutomate); value[ID::oscSync] = std::make_unique( Scales::defaultScale.invmap(1.0), Scales::defaultScale, "oscSync", @@ -195,6 +229,29 @@ struct GlobalParameter : public ParameterInterface { value[ID::release] = std::make_unique(1, Scales::boolScale, "release", Info::kCanAutomate); + value[ID::filterSwitch] = std::make_unique( + 0, Scales::boolScale, "filterSwitch", Info::kCanAutomate); + value[ID::filterDecayRatio] = std::make_unique( + Scales::filterDecayRatio.invmap(0.0), Scales::filterDecayRatio, "filterDecayRatio", + Info::kCanAutomate); + value[ID::filterCutoffBaseOctave] = std::make_unique( + Scales::filterCutoffBaseOctave.invmap(0.0), Scales::filterCutoffBaseOctave, + "filterCutoffBaseOctave", Info::kCanAutomate); + value[ID::filterCutoffModOctave] = std::make_unique( + Scales::filterCutoffModOctave.invmap(0.0), Scales::filterCutoffModOctave, + "filterCutoffModOctave", Info::kCanAutomate); + value[ID::filterResonanceBase] = std::make_unique( + Scales::defaultScale.invmap(0.1), Scales::defaultScale, "filterResonanceBase", + Info::kCanAutomate); + value[ID::filterResonanceMod] = std::make_unique( + Scales::defaultScale.invmap(0.0), Scales::defaultScale, "filterResonanceMod", + Info::kCanAutomate); + value[ID::filterNotchBaseOctave] = std::make_unique( + Scales::filterNotchBaseOctave.invmap(1.0), Scales::filterNotchBaseOctave, + "filterNotchBaseOctave", Info::kCanAutomate); + value[ID::filterNotchModOctave] = std::make_unique( + 0.0, Scales::filterNotchModOctave, "filterNotchModOctave", Info::kCanAutomate); + for (size_t idx = 0; idx < nPolyOscControl; ++idx) { auto indexStr = std::to_string(idx); auto ratio = double(idx + 1) / double(nPolyOscControl + 1); @@ -238,6 +295,8 @@ struct GlobalParameter : public ParameterInterface { value[ID::unisonPanSpread] = std::make_unique( Scales::defaultScale.invmap(1.0), Scales::defaultScale, "unisonPanSpread", Info::kCanAutomate); + value[ID::unisonScatterArpeggio] = std::make_unique( + 1, Scales::boolScale, "unisonScatterArpeggio", Info::kCanAutomate); for (size_t idx = 0; idx < nReservedParameter; ++idx) { auto indexStr = std::to_string(idx); diff --git a/common/gui/knob.hpp b/common/gui/knob.hpp index e0aec9de..116cc6c6 100644 --- a/common/gui/knob.hpp +++ b/common/gui/knob.hpp @@ -288,7 +288,11 @@ class TextKnob : public KnobBase { } else if (event.buttonState.isMiddle()) { if (event.modifiers.has(ModifierKey::Shift)) { beginEdit(); - value = scale.invmap(std::floor(scale.map(value))); + if (isDecibel) { + value = scale.invmap(dbToAmp(std::floor(ampToDB(scale.map(value))))); + } else { + value = scale.invmap(std::floor(scale.map(value))); + } valueChanged(); endEdit(); } else { @@ -305,6 +309,9 @@ class TextKnob : public KnobBase { void setPrecision(uint32_t precision) { this->precision = precision; } protected: + template inline T dbToAmp(T dB) { return std::pow(T(10), dB / T(20)); } + template inline T ampToDB(T amp) { return T(20) * std::log10(amp); } + float borderWidth = 1.0f; uint32_t precision = 0;