diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04f4d7eb..475643f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: - 'master' - 'docs' paths-ignore: - - 'README.md' + - '**.md' - 'docs/**' tags: - '*' diff --git a/GlitchSprinkler/source/dsp/dspcore.cpp b/GlitchSprinkler/source/dsp/dspcore.cpp index a8b8b368..0b1d75bf 100644 --- a/GlitchSprinkler/source/dsp/dspcore.cpp +++ b/GlitchSprinkler/source/dsp/dspcore.cpp @@ -24,6 +24,56 @@ #include #include +// `for (let i = 0; i < 12; ++i) console.log(2**(i / 12));` +std::array tuningRatioEt12 = { + double(1.00000000000000), double(1.0594630943592953), double(1.122462048309373), + double(1.189207115002721), double(1.2599210498948732), double(1.3348398541700344), + double(1.4142135623730951), double(1.4983070768766815), double(1.5874010519681994), + double(1.681792830507429), double(1.7817974362806785), double(1.887748625363387), +}; + +// ```javascript +// var cents = [0, 20, 240, 260, 360, 480, 500, 720, 740, 960, 980, 1180]; +// console.log(cents.map(v => 2**(v / 1200))); +// ``` +std::array tuningRatioEt5 = { + double(1.0000000000000), double(1.0116194403019225), double(1.148698354997035), + double(1.1620455869578397), double(1.2311444133449163), double(1.3195079107728942), + double(1.3348398541700344), double(1.515716566510398), double(1.5333283446696007), + double(1.7411011265922482), double(1.761331747192297), double(1.9770280407057923), +}; + +// cents = [0, 120, 240, 300, 360, 480, 600, 720, 840, 960, 1020, 1080]; +std::array tuningRatioEt10Minor = { + double(1.0000000000000), double(1.0717734625362931), double(1.148698354997035), + double(1.189207115002721), double(1.2311444133449163), double(1.3195079107728942), + double(1.4142135623730951), double(1.515716566510398), double(1.624504792712471), + double(1.7411011265922482), double(1.8025009252216604), double(1.8660659830736148), +}; + +std::array tuningRatioJust5Major = { + double(1) / double(1), double(16) / double(15), double(9) / double(8), + double(6) / double(5), double(5) / double(4), double(4) / double(3), + double(45) / double(32), double(3) / double(2), double(8) / double(5), + double(5) / double(3), double(16) / double(9), double(15) / double(8), +}; + +std::array tuningRatioJust5Minor = { + double(1) / double(1), double(16) / double(15), double(10) / double(9), + double(6) / double(5), double(5) / double(4), double(4) / double(3), + double(64) / double(45), double(3) / double(2), double(8) / double(5), + double(5) / double(3), double(9) / double(5), double(15) / double(8), +}; + +// TODO: remove 1 element if possible. +std::array tuningRatioJust7 = { + double(1) / double(1), double(8) / double(7), double(7) / double(6), + double(6) / double(5), double(5) / double(4), double(4) / double(3), + double(7) / double(5), double(10) / double(7), double(3) / double(2), + double(8) / double(5), double(5) / double(3), double(12) / double(7), + double(7) / double(4), +}; + void DSPCore::setup(double sampleRate) { noteStack.reserve(1024); @@ -56,6 +106,11 @@ void DSPCore::reset() ASSIGN_PARAMETER(reset); + scheduleUpdateNote = false; + phaseCounter = 0; + decayGain = double(0); + polynomial.updateCoefficients(true); + startup(); } @@ -85,6 +140,9 @@ std::array DSPCore::processFrame() modPhase -= std::floor(modPhase); sig = polynomial.evaluate(modPhase); + // Saturation. + sig = std::clamp(saturationGain * sig, double(-1), double(1)); + // Envelope. decayGain *= decayRatio; const auto gain = decayGain * outGain; @@ -122,13 +180,38 @@ void DSPCore::noteOn(NoteInfo &info) { noteStack.push_back(info); - if (noteStack.size() == 1 && phaseCounter == 0) { + constexpr double epsF32 = double(std::numeric_limits::epsilon()); + if (decayGain < epsF32 || (noteStack.size() == 1 && phaseCounter == 0)) { updateNote(); } else { scheduleUpdateNote = true; } } +inline double getPitchRatio(Tuning tuning, int semitone, double octave, double cent) +{ + auto getTuning = [&](Tuning tuning) { + switch (tuning) { + default: + case Tuning::equalTemperament12: + return tuningRatioEt12; + case Tuning::equalTemperament5: + return tuningRatioEt5; + case Tuning::justIntonation5LimitMajor: + return tuningRatioJust5Major; + } + }; + auto tuningTable = getTuning(tuning); + + int size = int(tuningTable.size()); + int index = semitone % size; + if (index < 0) index += size; + + octave += double((semitone - index) / size); + + return tuningTable[index] * std::exp2(octave + cent / double(1200)); +} + void DSPCore::updateNote() { using ID = ParameterID::ID; @@ -139,20 +222,30 @@ void DSPCore::updateNote() velocity = velocityMap.map(info.velocity); - auto freqHz = std::min( - double(0.5) * sampleRate, - double(440) * std::exp2((info.noteNumber - double(69)) / double(12))); + // Pitch & phase. + auto transposeOctave = int(pv[ID::transposeOctave]->getInt()) - transposeOctaveOffset; + auto transposeSemitone + = int(pv[ID::transposeSemitone]->getInt()) - transposeSemitoneOffset; + auto transposeCent = pv[ID::transposeCent]->getDouble(); + auto transposeRatio = getPitchRatio( + static_cast(pv[ID::tuning]->getInt()), + transposeSemitone + info.noteNumber - 69, transposeOctave, transposeCent); + auto freqHz = std::min(double(0.5) * sampleRate, transposeRatio * double(440)); + phasePeriod = uint_fast32_t(sampleRate / freqHz); phaseCounter = 0; + // Oscillator. + polynomial.updateCoefficients(true); + oscSync = pv[ID::oscSync]->getDouble(); fmIndex = pv[ID::fmIndex]->getDouble(); + saturationGain = pv[ID::saturationGain]->getDouble(); + // Envelope. const auto decaySeconds = pv[ID::decaySeconds]->getDouble(); decayRatio = std::pow(double(1e-3), double(1) / (decaySeconds * sampleRate)); decayGain = double(1) / decayRatio; - - polynomial.updateCoefficients(true); } void DSPCore::noteOff(int_fast32_t noteId) diff --git a/GlitchSprinkler/source/dsp/dspcore.hpp b/GlitchSprinkler/source/dsp/dspcore.hpp index cafded67..e0740e68 100644 --- a/GlitchSprinkler/source/dsp/dspcore.hpp +++ b/GlitchSprinkler/source/dsp/dspcore.hpp @@ -34,7 +34,8 @@ class DSPCore { bool isNoteOn; uint32_t frame; int32_t id; - float noteNumber; + int noteNumber; + float cent; float velocity; }; @@ -71,7 +72,8 @@ class DSPCore { note.isNoteOn = isNoteOn; note.frame = frame; note.id = noteId; - note.noteNumber = noteNumber + tuning; + note.noteNumber = noteNumber; + note.cent = tuning; note.velocity = velocity; midiNotes.push_back(note); } @@ -108,13 +110,12 @@ class DSPCore { std::minstd_rand paramRng{0}; bool scheduleUpdateNote = false; - bool isReleasing = false; uint_fast32_t phasePeriod = 0; - uint_fast32_t phaseCounter = std::numeric_limits::max(); + uint_fast32_t phaseCounter = 0; double oscSync = double(1); double fmIndex = double(0); - double decayGain = double(1); + double saturationGain = double(1); + double decayGain = double(0); double decayRatio = double(1); - PolynomialCoefficientSolver polynomial; }; diff --git a/GlitchSprinkler/source/dsp/polynomial.hpp b/GlitchSprinkler/source/dsp/polynomial.hpp index f902c116..437a47c7 100644 --- a/GlitchSprinkler/source/dsp/polynomial.hpp +++ b/GlitchSprinkler/source/dsp/polynomial.hpp @@ -32,6 +32,9 @@ Solve `A x = b` for `x`. memory allocation for each call. */ template class Solver { +private: + static constexpr T epsilon = std::numeric_limits::epsilon(); + public: std::array, size> lu{}; std::array y{}; @@ -43,10 +46,10 @@ template class Solver { { lu = A; for (int i = 0; i < size; ++i) { - if (std::abs(A[i][i]) <= std::numeric_limits::epsilon()) { // Pivoting. + if (std::abs(A[i][i]) <= epsilon) { // Pivoting. int j = i + 1; for (; j < size; ++j) { - if (std::abs(A[j][i]) <= std::numeric_limits::epsilon()) continue; + if (std::abs(A[j][i]) <= epsilon) continue; std::swap(A[i], A[j]); std::swap(b[i], b[j]); break; @@ -63,6 +66,10 @@ template class Solver { for (int k = 0; k < i; ++k) sum += lu[i][k] * lu[k][j]; lu[i][j] = A[i][j] - sum; } + + // Avoid division by 0. This is only suitable for this application. + if (std::abs(lu[i][i]) < epsilon) lu[i][i] = std::copysign(epsilon, lu[i][i]); + for (int j = i + 1; j < size; ++j) { T sum = T(0); for (int k = 0; k < i; ++k) sum += lu[j][k] * lu[k][i]; @@ -113,7 +120,10 @@ template class PolynomialCoefficientSolve std::array coefficients{}; Sample normalizeGain = Sample(1); - Sample evaluate(Sample x) { return computePolynomial(x, coefficients); } + Sample evaluate(Sample x) + { + return computePolynomial(x, coefficients); + } void updateCoefficients(bool normalize = false) { @@ -152,7 +162,8 @@ template class PolynomialCoefficientSolve for (size_t i = 0; i < d1.size(); ++i) d1[i] = Sample(i + 1) * coefficients[i + 1]; auto getPeakPoint = [&](Sample x) { - return std::array{x, std::abs(computePolynomial(x, coefficients))}; + return std::array{ + x, std::abs(computePolynomial(x, coefficients))}; }; auto sgn = [](Sample x) { return x > 0 ? Sample(1) : x < 0 ? Sample(-1) : x; }; constexpr size_t maxIteration = 53; // Number of significand bits. @@ -164,8 +175,8 @@ template class PolynomialCoefficientSolve size_t iter = 0; do { - Sample yL = computePolynomial(xL, d1); - Sample yR = computePolynomial(xR, d1); + Sample yL = computePolynomial(xL, d1); + Sample yR = computePolynomial(xR, d1); Sample signL = sgn(yL); Sample signR = sgn(yR); @@ -177,7 +188,7 @@ template class PolynomialCoefficientSolve } xM = 0.5 * (xR + xL); - Sample yM = computePolynomial(xM, d1); + Sample yM = computePolynomial(xM, d1); Sample signM = sgn(yM); if (signM == 0) { diff --git a/GlitchSprinkler/source/editor.cpp b/GlitchSprinkler/source/editor.cpp index 0bfeb684..4811cce5 100644 --- a/GlitchSprinkler/source/editor.cpp +++ b/GlitchSprinkler/source/editor.cpp @@ -88,15 +88,6 @@ void Editor::updateUI(ParamID id, ParamValue normalized) PlugEditor::updateUI(id, normalized); using ID = Synth::ParameterID::ID; - - if (id >= ID::polynomialPointX0 && id < ID::polynomialPointX0 + nPolyOscControl) { - polynomialXYPad->setXAt(id - ID::polynomialPointX0, normalized); - polynomialXYPad->setDirty(); - } else if (id >= ID::polynomialPointY0 && id < ID::polynomialPointY0 + nPolyOscControl) - { - polynomialXYPad->setYAt(id - ID::polynomialPointY0, normalized); - polynomialXYPad->setDirty(); - } } bool Editor::prepareUI() @@ -123,6 +114,7 @@ bool Editor::prepareUI() constexpr auto mixTop9 = mixTop0 + 9 * labelY; constexpr auto mixTop10 = mixTop0 + 10 * labelY; constexpr auto mixTop11 = mixTop0 + 11 * labelY; + constexpr auto mixTop12 = mixTop0 + 12 * labelY; constexpr auto mixLeft0 = left0; constexpr auto mixLeft1 = mixLeft0 + labelWidth + 2 * margin; addGroupLabel( @@ -133,15 +125,6 @@ bool Editor::prepareUI() mixLeft1, mixTop1, labelWidth, labelHeight, uiTextSize, ID::outputGain, Scales::gain, true, 5); - addLabel(mixLeft0, mixTop2, labelWidth, labelHeight, uiTextSize, "Seed"); - auto seedTextKnob = addTextKnob( - mixLeft1, mixTop2, 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(mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "Decay [s]"); addTextKnob( mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, ID::decaySeconds, @@ -154,6 +137,47 @@ bool Editor::prepareUI() addTextKnob( mixLeft1, mixTop5, labelWidth, labelHeight, uiTextSize, ID::fmIndex, Scales::fmIndex, false, 5); + addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Saturation [dB]"); + addTextKnob( + mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::saturationGain, + Scales::gain, true, 5); + + addLabel(mixLeft0, mixTop8, labelWidth, labelHeight, uiTextSize, "Seed"); + auto seedTextKnob = addTextKnob( + mixLeft1, mixTop8, 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(mixLeft0, mixTop9, labelWidth, labelHeight, uiTextSize, "Octave"); + addTextKnob( + mixLeft1, mixTop9, labelWidth, labelHeight, uiTextSize, ID::transposeOctave, + Scales::transposeOctave, false, 0, -transposeOctaveOffset); + addLabel(mixLeft0, mixTop10, labelWidth, labelHeight, uiTextSize, "Semitone"); + addTextKnob( + mixLeft1, mixTop10, labelWidth, labelHeight, uiTextSize, ID::transposeSemitone, + Scales::transposeSemitone, false, 0, -transposeSemitoneOffset); + addLabel(mixLeft0, mixTop11, labelWidth, labelHeight, uiTextSize, "Cent"); + addTextKnob( + mixLeft1, mixTop11, labelWidth, labelHeight, uiTextSize, ID::transposeCent, + Scales::transposeCent, false, 5); + addLabel(mixLeft0, mixTop12, labelWidth, labelHeight, uiTextSize, "Tuning"); + addOptionMenu( + mixLeft1, mixTop12, labelWidth, labelHeight, uiTextSize, ID::tuning, + { + "Equal Temperament 12", "Equal Temperament 5", "Just Intonation 5-limit Major", + "- Reserved 03 -", "- Reserved 04 -", "- 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 -", "- Reserved 32 -", + }); // Waveform. constexpr auto waveformTop0 = top0 + 0 * labelY; @@ -162,18 +186,31 @@ bool Editor::prepareUI() addGroupLabel( waveformLeft0, waveformTop0, groupLabelWidth, labelHeight, uiTextSize, "Waveform"); - polynomialXYPad = new PolynomialXYPad( - CRect( - waveformLeft0, waveformTop1, waveformLeft0 + barBoxWidth, - waveformTop1 + barBoxWidth), - this, 0, palette, this); - for (size_t idx = 0; idx < nPolyOscControl; ++idx) { - polynomialXYPad->setXAt( - idx, controller->getParamNormalized(ID::polynomialPointX0 + idx)); - polynomialXYPad->setYAt( - idx, controller->getParamNormalized(ID::polynomialPointY0 + idx)); + { + std::vector id(2 * nPolyOscControl); + for (size_t i = 0; i < id.size(); ++i) id[i] = ID::polynomialPointX0 + ParamID(i); + std::vector value(id.size()); + for (size_t i = 0; i < value.size(); ++i) { + value[i] = controller->getParamNormalized(id[i]); + } + std::vector defaultValue(id.size()); + for (size_t i = 0; i < defaultValue.size(); ++i) { + defaultValue[i] = param->getDefaultNormalized(id[i]); + } + polynomialXYPad = new PolynomialXYPad( + this, + CRect( + waveformLeft0, waveformTop1, waveformLeft0 + barBoxWidth, + waveformTop1 + barBoxWidth), + id, value, defaultValue, palette); + + addToArrayControlInstances(id[0], polynomialXYPad); + + for (ParamID i = 0; i < id.size(); ++i) { + arrayControlMap.emplace(std::make_pair(id[i], polynomialXYPad)); + } + frame->addView(polynomialXYPad); } - frame->addView(polynomialXYPad); // Randomize button. const auto randomButtonTop = top0 + 18 * labelY; diff --git a/GlitchSprinkler/source/gui/polynomialxypad.hpp b/GlitchSprinkler/source/gui/polynomialxypad.hpp index 84308c04..b1ab088f 100644 --- a/GlitchSprinkler/source/gui/polynomialxypad.hpp +++ b/GlitchSprinkler/source/gui/polynomialxypad.hpp @@ -32,22 +32,60 @@ namespace VSTGUI { -class PolynomialXYPad : public CControl { +class PolynomialXYPad : public ArrayControl { +private: + Uhhyou::Palette &pal; + + static constexpr double twopi = double(2) * std::numbers::pi_v; + static constexpr CCoord controlRadiusHalfOuter = 8.0; + static constexpr CCoord controlRadiusHalfInner = controlRadiusHalfOuter / 2; + static constexpr CCoord controlRadiusFull = 2 * controlRadiusHalfOuter; + CCoord borderWidth = 1.0; + + CPoint mousePosition{-1.0, -1.0}; + bool isMouseDown = false; + bool isMouseEntered = false; + + static constexpr size_t nControlPoint = nPolyOscControl; + std::array controlPoints; + int focusedPoint = -1; + int grabbedPoint = -1; + + bool requireUpdate = false; + SomeDSP::PolynomialCoefficientSolver polynomial; + public: PolynomialXYPad( + Steinberg::Vst::VSTGUIEditor *editor, const CRect &size, - IControlListener *listener, - int32_t tag, - Uhhyou::Palette &palette, - Steinberg::Vst::PlugEditor *editor) - : CControl(size, listener, tag), pal(palette), editor(editor), requireUpdate(true) + std::vector id, + std::vector value, + std::vector defaultValue, + Uhhyou::Palette &palette) + : ArrayControl(editor, size, id, value, defaultValue) + , pal(palette) + , requireUpdate(true) { - if (editor) editor->remember(); + setWantsFocus(true); + + updateControlPoints(); } - ~PolynomialXYPad() + virtual void setValueAt(Steinberg::Vst::ParamID id_, double normalized) override { - if (editor) editor->forget(); + ArrayControl::setValueAt(id_, normalized); + + using ID = Steinberg::Synth::ParameterID::ID; + + auto iter = idMap.find(id_); + if (iter == idMap.end()) return; + auto index = iter->second; + + if (index < nControlPoint) { + setXAt(index, normalized); + } else { + setYAt(index % nControlPoint, normalized); + } } void draw(CDrawContext *pContext) override @@ -178,8 +216,8 @@ class PolynomialXYPad : public CControl { } using ID = Steinberg::Synth::ParameterID::ID; - setParam(ID::polynomialPointX0 + grabbedPoint, point.x / getWidth()); - setParam(ID::polynomialPointY0 + grabbedPoint, point.y / getHeight()); + editAndUpdateValueAt(ID::polynomialPointX0 + grabbedPoint, point.x / getWidth()); + editAndUpdateValueAt(ID::polynomialPointY0 + grabbedPoint, point.y / getHeight()); requireUpdate = true; } else { @@ -201,9 +239,13 @@ class PolynomialXYPad : public CControl { event.consumed = true; } - void scheduleUpdate() { requireUpdate = true; } void setBorderWidth(CCoord width) { borderWidth = width < 0 ? 0 : width; } + CLASS_METHODS(PolynomialXYPad, CControl); + +private: + void scheduleUpdate() { requireUpdate = true; } + void setXAt(size_t index, double x) { x = std::clamp(x, double(0), double(1)); @@ -218,12 +260,12 @@ class PolynomialXYPad : public CControl { scheduleUpdate(); } - CLASS_METHODS(PolynomialXYPad, CControl); - -private: - inline void setParam(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue value) + void updateControlPoints() { - if (editor) editor->valueChanged(id, value); + for (size_t idx = 0; idx < nControlPoint; ++idx) { + setXAt(idx, value[idx]); + setYAt(idx, value[idx + nControlPoint]); + } } void setMousePosition(CPoint &pos) @@ -252,28 +294,6 @@ class PolynomialXYPad : public CControl { } polynomial.updateCoefficients(); } - - Steinberg::Vst::PlugEditor *editor = nullptr; - - Uhhyou::Palette &pal; - - static constexpr double twopi = double(2) * std::numbers::pi_v; - static constexpr CCoord controlRadiusHalfOuter = 8.0; - static constexpr CCoord controlRadiusHalfInner = controlRadiusHalfOuter / 2; - static constexpr CCoord controlRadiusFull = 2 * controlRadiusHalfOuter; - CCoord borderWidth = 1.0; - - CPoint mousePosition{-1.0, -1.0}; - bool isMouseDown = false; - bool isMouseEntered = false; - - static constexpr size_t nControlPoint = nPolyOscControl; - std::array controlPoints; - int focusedPoint = -1; - int grabbedPoint = -1; - - bool requireUpdate = false; - SomeDSP::PolynomialCoefficientSolver polynomial; }; } // namespace VSTGUI diff --git a/GlitchSprinkler/source/parameter.cpp b/GlitchSprinkler/source/parameter.cpp index 85c39d6f..934c4235 100644 --- a/GlitchSprinkler/source/parameter.cpp +++ b/GlitchSprinkler/source/parameter.cpp @@ -41,5 +41,10 @@ DecibelScale Scales::fmIndex(-60.0, 40.0, true); LinearScale Scales::polynomialPointY(-0.5, 0.5); +UIntScale Scales::transposeOctave(2 * transposeOctaveOffset); +UIntScale Scales::transposeSemitone(2 * transposeSemitoneOffset); +LinearScale Scales::transposeCent(-3600, 3600); +UIntScale Scales::tuning(31); + } // namespace Synth } // namespace Steinberg diff --git a/GlitchSprinkler/source/parameter.hpp b/GlitchSprinkler/source/parameter.hpp index 7b698e34..1eba5865 100644 --- a/GlitchSprinkler/source/parameter.hpp +++ b/GlitchSprinkler/source/parameter.hpp @@ -36,9 +36,20 @@ static constexpr size_t nPolyOscControl = 13; constexpr size_t nReservedParameter = 256; constexpr size_t nReservedGuiParameter = 64; +constexpr int transposeOctaveOffset = 10; +constexpr int transposeSemitoneOffset = 36; + namespace Steinberg { namespace Synth { +enum Tuning { + equalTemperament12, + equalTemperament5, + justIntonation5LimitMajor, + + Tuning_ENUM_LENGTH, +}; + namespace ParameterID { enum ID { bypass, @@ -48,8 +59,13 @@ enum ID { decaySeconds, oscSync, fmIndex, + saturationGain, seed, + transposeOctave, + transposeSemitone, + transposeCent, + tuning, polynomialPointX0, polynomialPointY0 = polynomialPointX0 + nPolyOscControl, @@ -74,6 +90,11 @@ struct Scales { static SomeDSP::DecibelScale fmIndex; static SomeDSP::LinearScale polynomialPointY; + + static SomeDSP::UIntScale transposeOctave; + static SomeDSP::UIntScale transposeSemitone; + static SomeDSP::LinearScale transposeCent; + static SomeDSP::UIntScale tuning; }; struct GlobalParameter : public ParameterInterface { @@ -103,9 +124,22 @@ struct GlobalParameter : public ParameterInterface { Info::kCanAutomate); value[ID::fmIndex] = std::make_unique( Scales::fmIndex.invmap(0.0), Scales::fmIndex, "fmIndex", Info::kCanAutomate); + value[ID::saturationGain] = std::make_unique( + Scales::gain.invmap(1.0), Scales::gain, "saturationGain", Info::kCanAutomate); value[ID::seed] = std::make_unique(0, Scales::seed, "seed", Info::kCanAutomate); + value[ID::transposeOctave] = std::make_unique( + transposeOctaveOffset, Scales::transposeOctave, "transposeOctave", + Info::kCanAutomate); + value[ID::transposeSemitone] = std::make_unique( + transposeSemitoneOffset, Scales::transposeSemitone, "transposeSemitone", + Info::kCanAutomate); + value[ID::transposeCent] = std::make_unique( + Scales::transposeCent.invmap(0.0), Scales::transposeCent, "transposeCent", + Info::kCanAutomate); + value[ID::tuning] + = std::make_unique(0, Scales::tuning, "tuning", Info::kCanAutomate); for (size_t idx = 0; idx < nPolyOscControl; ++idx) { auto indexStr = std::to_string(idx); diff --git a/common/gui/arraycontrol.hpp b/common/gui/arraycontrol.hpp index 09cdb19c..29ee884b 100644 --- a/common/gui/arraycontrol.hpp +++ b/common/gui/arraycontrol.hpp @@ -108,7 +108,7 @@ struct ArrayControl : public CView, public IFocusDrawing { if (index >= isEditing.size() || !getFrame()) return; if (!isEditing[index]) return; isEditing[index] = false; - getFrame()->beginEdit(id[index]); + getFrame()->endEdit(id[index]); } virtual void setValueAt(Steinberg::Vst::ParamID id, double normalized) @@ -125,6 +125,17 @@ struct ArrayControl : public CView, public IFocusDrawing { endEdit(); } + void editAndUpdateValueAt(Steinberg::Vst::ParamID id, double normalized) + { + auto iter = idMap.find(id); + if (iter == idMap.end()) return; + auto index = iter->second; + beginEdit(index); + setValueAt(id, normalized); + updateValueAt(index); + endEdit(index); + } + void updateValue() { if (id.size() != value.size()) return;