diff --git a/GlitchSprinkler/source/dsp/dspcore.cpp b/GlitchSprinkler/source/dsp/dspcore.cpp index 44e86a91..9c1df4d5 100644 --- a/GlitchSprinkler/source/dsp/dspcore.cpp +++ b/GlitchSprinkler/source/dsp/dspcore.cpp @@ -39,6 +39,9 @@ void DSPCore::setup(double sampleRate_) terminationLength = uint_fast32_t(double(0.002) * sampleRate); lowpassInterpRate = sampleRate / double(48000 * 64); + softAttackEmaRatio = EMAFilter::cutoffToP(double(50) / sampleRate); + + for (auto &x : safetyFilter) x.setup(sampleRate); reset(); startup(); @@ -67,13 +70,15 @@ void DSPCore::setup(double sampleRate_) \ const auto samplesPerBeat = (double(60) * sampleRate) / tempo; \ const auto arpeggioNotesPerBeat = pv[ID::arpeggioNotesPerBeat]->getInt() + 1; \ - arpeggioDuration = int_fast32_t(samplesPerBeat / double(arpeggioNotesPerBeat)); \ + arpeggioNoteDuration = int_fast32_t(samplesPerBeat / double(arpeggioNotesPerBeat)); \ arpeggioLoopLength \ = (pv[ID::arpeggioLoopLengthInBeat]->getInt()) * arpeggioNotesPerBeat; \ if (arpeggioLoopLength == 0) { \ arpeggioLoopLength = std::numeric_limits::max(); \ } \ \ + pwmRatio = double(1) - pv[ID::pulseWidthRatio]->getDouble(); \ + safetyFilterMix.METHOD(pv[ID::safetyFilterMix]->getDouble()); \ outputGain.METHOD(pv[ID::outputGain]->getDouble()); \ \ for (size_t idx = 0; idx < nPolyOscControl; ++idx) { \ @@ -109,6 +114,10 @@ void Voice::reset() terminationCounter = 0; scheduleUpdateNote = false; + pwmLower = 0; + pwmPoint = 0; + pwmBitMask = 0; + pwmDirection = 1; phasePeriod = 0; phaseCounter = 0; oscSync = double(1); @@ -116,6 +125,8 @@ void Voice::reset() saturationGain = double(1); decayRatio = double(1); decayGain = double(0); + decayEmaRatio = double(1); + decayEmaValue = double(0); polynomialCoefficients.fill({}); filterDecayRatio = double(1); @@ -144,6 +155,8 @@ void DSPCore::reset() nextSteal = 0; for (auto &x : voices) x.reset(); + for (auto &x : safetyFilter) x.reset(); + startup(); } @@ -171,7 +184,7 @@ std::array Voice::processFrame() // Arpeggio. if (pv[ID::arpeggioSwitch]->getInt()) { - if (arpeggioTimer < arpeggioTie * core.arpeggioDuration) { + if (arpeggioTimer < arpeggioTie * core.arpeggioNoteDuration) { ++arpeggioTimer; } else if (state == State::active) { scheduleUpdateNote = true; @@ -201,11 +214,12 @@ std::array Voice::processFrame() // Oscillator. auto oscFunc = [&](double phase) { + if (phaseCounter > (pwmPoint | ~pwmBitMask)) return double(0); return computePolynomial( phase, polynomialCoefficients); }; - const auto phase = oscSync * double(phaseCounter) / double(phasePeriod); + const auto phase = oscSync * double(phaseCounter & pwmBitMask) / double(phasePeriod); auto sig = oscFunc(phase); // FM. @@ -227,13 +241,30 @@ std::array Voice::processFrame() // Envelope. decayGain *= decayRatio; + decayEmaValue += decayEmaRatio * (decayGain - decayEmaValue); const auto terminationDecay = double(core.terminationLength - terminationCounter) / double(core.terminationLength); - lastGain = decayGain * noteVelocity * terminationDecay; + lastGain = decayEmaValue * noteVelocity * terminationDecay; sig *= lastGain; if (++phaseCounter >= phasePeriod) { phaseCounter = 0; + + if (pv[ID::pulseWidthModulation]->getInt()) { + pwmPoint = std::min(pwmPoint + pwmDirection, phasePeriod); + if (pwmPoint <= pwmLower) { + pwmDirection = 1; + } else if (pwmPoint == phasePeriod) { + pwmDirection = -1; + } + } else { + pwmPoint = uint_fast32_t(phasePeriod * core.pwmRatio); + } + + pwmBitMask = pv[ID::pulseWidthBitwiseAnd]->getInt() + ? pwmPoint + : std::numeric_limits::max(); + if (scheduleUpdateNote) { scheduleUpdateNote = false; if (state == State::active) updateNote(); @@ -277,6 +308,10 @@ void DSPCore::process(const size_t length, float *out0, float *out1) frame[1] += voiceOut[1]; } + const auto safetyFiltMix = safetyFilterMix.process(); + frame[0] = std::lerp(frame[0], safetyFilter[0].process(frame[0]), safetyFiltMix); + frame[1] = std::lerp(frame[1], safetyFilter[1].process(frame[1]), safetyFiltMix); + const auto outGain = outputGain.process(); out0[i] = float(outGain * frame[0]); out1[i] = float(outGain * frame[1]); @@ -334,11 +369,15 @@ void DSPCore::noteOn(NoteInfo &info) if (vc.state == Voice::State::rest) { vc.arpeggioTimer = 0; vc.arpeggioLoopCounter = 0; + vc.decayEmaRatio + = pv[ID::decaySoftAttack]->getInt() ? softAttackEmaRatio : double(1); + vc.decayEmaValue = 0; vc.lowpass.reset(); vc.updateNote(); } else if (!param.value[ParameterID::arpeggioSwitch]->getInt()) { vc.scheduleUpdateNote = true; } + vc.terminationCounter = 0; vc.state = Voice::State::active; } @@ -475,8 +514,8 @@ void Voice::updateNote() return core.pitchModifier * transposeRatio * double(440); }; auto getDiscreteFreq = [&]() { - auto semitones = transposeSemitone + double(noteNumber - 127) / double(12); - auto cents = (transposeCent + noteCent) / double(1200); + auto semitones = double(transposeSemitone + noteNumber - 127) / double(12); + auto cents = (transposeCent + noteCent + unisonDetuneCent) / double(1200); auto transposeRatio = std::exp2(transposeOctave + semitones + cents); auto freqHz = core.pitchModifier * transposeRatio * core.sampleRate; switch (tuning) { @@ -504,7 +543,8 @@ void Voice::updateNote() auto arpRatio = std::exp2(double(octaveDist(rngArpeggio)) + centDist(rngArpeggio) / double(1200)); - if (arpeggioTimer != 0 || arpeggioLoopCounter != 0) { + const auto arpeggioStartFromRoot = bool(pv[ID::arpeggioStartFromRoot]->getInt()); + if (!(arpeggioStartFromRoot && arpeggioTimer == 0 && arpeggioLoopCounter == 0)) { const auto arpeggioScale = pv[ID::arpeggioScale]->getInt(); if (arpeggioScale == PitchScale::et5Chromatic) { arpRatio *= getRandomPitchFixed(rngArpeggio, scaleEt5, tuningRatioEt5); @@ -577,6 +617,19 @@ void Voice::updateNote() } polynomialCoefficients = core.polynomial.coefficients; + pwmLower = uint_fast32_t(phasePeriod * core.pwmRatio); + if ( + state == State::rest || core.pwmRatio >= 1 + || pv[ID::arpeggioResetModulation]->getInt()) + { + pwmPoint = pv[ID::pulseWidthModulation]->getInt() ? phasePeriod : pwmLower; + pwmDirection = 1; + + pwmBitMask = pv[ID::pulseWidthBitwiseAnd]->getInt() + ? pwmPoint + : std::numeric_limits::max(); + } + oscSync = pv[ID::oscSync]->getDouble(); const auto rndFmIndexOct = pv[ID::randomizeFmIndex]->getDouble(); @@ -587,9 +640,11 @@ void Voice::updateNote() // Envelope. const auto decayTargetGain = pv[ID::decayTargetGain]->getDouble(); decayRatio - = std::pow(decayTargetGain, double(1) / (arpeggioTie * core.arpeggioDuration)); + = std::pow(decayTargetGain, double(1) / (arpeggioTie * core.arpeggioNoteDuration)); - const auto restChance = pv[ID::arpeggioRestChance]->getDouble(); + const auto restChance = arpeggioTimer == 0 && arpeggioLoopCounter == 0 + ? double(0) + : pv[ID::arpeggioRestChance]->getDouble(); std::uniform_real_distribution restDist{double(0), double(1)}; decayGain = useArpeggiator && restDist(rngArpeggio) < restChance || freqHz == 0 ? double(0) diff --git a/GlitchSprinkler/source/dsp/dspcore.hpp b/GlitchSprinkler/source/dsp/dspcore.hpp index 57e46404..fee85495 100644 --- a/GlitchSprinkler/source/dsp/dspcore.hpp +++ b/GlitchSprinkler/source/dsp/dspcore.hpp @@ -71,6 +71,10 @@ class Voice { uint_fast32_t terminationCounter = 0; bool scheduleUpdateNote = false; + uint_fast32_t pwmLower = 0; + uint_fast32_t pwmPoint = 0; + uint_fast32_t pwmBitMask = 0; + int_fast32_t pwmDirection = 1; uint_fast32_t phasePeriod = 0; uint_fast32_t phaseCounter = 0; double oscSync = double(1); @@ -78,6 +82,8 @@ class Voice { double saturationGain = double(1); double decayRatio = double(1); double decayGain = double(0); + double decayEmaRatio = double(1); + double decayEmaValue = double(0); std::array polynomialCoefficients{}; double filterDecayRatio = double(1); @@ -115,12 +121,15 @@ class DSPCore { bool isPolyphonic = true; Voice::State noteOffState = Voice::State::terminate; - uint_fast32_t arpeggioDuration = std::numeric_limits::max(); + uint_fast32_t arpeggioNoteDuration = std::numeric_limits::max(); uint_fast32_t arpeggioLoopLength = std::numeric_limits::max(); uint_fast32_t terminationLength = 0; double lowpassInterpRate = double(1) / double(64); + double softAttackEmaRatio = double(1); + double pwmRatio = double(1); DecibelScale velocityMap{-60, 0, true}; + ExpSmoother safetyFilterMix; ExpSmoother outputGain; bool isPolynomialUpdated = false; @@ -137,6 +146,8 @@ class DSPCore { unsigned nextSteal = 0; std::vector voices; + std::array, 2> safetyFilter; + DSPCore() { midiNotes.reserve(2048); diff --git a/GlitchSprinkler/source/dsp/polynomial.hpp b/GlitchSprinkler/source/dsp/polynomial.hpp index bd0c124f..0d3c0d26 100644 --- a/GlitchSprinkler/source/dsp/polynomial.hpp +++ b/GlitchSprinkler/source/dsp/polynomial.hpp @@ -221,6 +221,8 @@ template class PolynomialCoefficientSolve template class ResonantEmaLowpass1A1 { private: + static constexpr Sample nyquist = Sample(0.4999); + Sample cutValue = Sample(1); Sample apsValue = Sample(1); Sample resValue = Sample(0); @@ -249,13 +251,13 @@ template class ResonantEmaLowpass1A1 { 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 freq = pi * std::clamp(cutValue, Sample(0), nyquist); const auto s = Sample(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 t = std::tan(std::clamp(apsValue * freq, Sample(0), nyquist)); const auto c2 = (t - 1) / (t + 1); resValue += interpRate * (resonance - resValue); @@ -270,4 +272,41 @@ template class ResonantEmaLowpass1A1 { } }; +// Bilinear transformed 1-pole highpass. +template class SafetyFilter { +private: + Sample b = Sample(1); + Sample a1 = Sample(1); + Sample x1 = Sample(0); + Sample y1 = Sample(0); + +public: + void reset() + { + x1 = 0; + y1 = 0; + } + + void setup(Sample sampleRate) + { + constexpr Sample pi = std::numbers::pi_v; + constexpr Sample minCutoff = Sample(0.00001); + constexpr Sample nyquist = Sample(0.49998); + constexpr Sample highpassCutoffHz = Sample(16); + + const auto k = Sample(1) + / std::tan(pi * std::clamp(highpassCutoffHz / sampleRate, minCutoff, nyquist)); + const auto a0 = Sample(1) + k; + b = k / a0; + a1 = (Sample(1) - k) / a0; + } + + Sample process(Sample input) + { + y1 = b * (input - x1) - a1 * y1; + x1 = input; + return y1; + } +}; + } // namespace SomeDSP diff --git a/GlitchSprinkler/source/editor.cpp b/GlitchSprinkler/source/editor.cpp index 14f9a71c..ba1c2923 100644 --- a/GlitchSprinkler/source/editor.cpp +++ b/GlitchSprinkler/source/editor.cpp @@ -108,26 +108,38 @@ bool Editor::prepareUI() addTextKnob( mixLeft1, mixTop1, labelWidth, labelHeight, uiTextSize, ID::outputGain, Scales::gain, true, 5); - - addLabel(mixLeft0, mixTop3, labelWidth, labelHeight, uiTextSize, "Decay to [dB]"); + addLabel(mixLeft0, mixTop2, labelWidth, labelHeight, uiTextSize, "Safety Filter Mix"); addTextKnob( - mixLeft1, mixTop3, labelWidth, labelHeight, uiTextSize, ID::decayTargetGain, + mixLeft1, mixTop2, labelWidth, labelHeight, uiTextSize, ID::safetyFilterMix, + Scales::defaultScale, false, 5); + + addLabel(mixLeft0, mixTop4, labelWidth, labelHeight, uiTextSize, "Decay to [dB]"); + addTextKnob( + mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, ID::decayTargetGain, Scales::decayTargetGain, true, 5); + addCheckbox( + mixLeft0, mixTop5, labelWidth, labelHeight, uiTextSize, "Soft Attack", + ID::decaySoftAttack); - addLabel(mixLeft0, mixTop5, labelWidth, labelHeight, uiTextSize, "Octave"); + addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Octave"); addTextKnob( - mixLeft1, mixTop5, labelWidth, labelHeight, uiTextSize, ID::transposeOctave, + mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::transposeOctave, Scales::transposeOctave, false, 0, -transposeOctaveOffset); - addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Semitone"); + addLabel(mixLeft0, mixTop7, labelWidth, labelHeight, uiTextSize, "Semitone"); addTextKnob( - mixLeft1, mixTop6, labelWidth, labelHeight, uiTextSize, ID::transposeSemitone, + mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::transposeSemitone, Scales::transposeSemitone, false, 0, -transposeSemitoneOffset); - addLabel(mixLeft0, mixTop7, labelWidth, labelHeight, uiTextSize, "Cent"); - addTextKnob( - mixLeft1, mixTop7, labelWidth, labelHeight, uiTextSize, ID::transposeCent, + addLabel(mixLeft0, mixTop8, labelWidth, labelHeight, uiTextSize, "Cent"); + auto transposeCentTextKnob = addTextKnob( + mixLeft1, mixTop8, labelWidth, labelHeight, uiTextSize, ID::transposeCent, Scales::transposeCent, false, 5); + if (transposeCentTextKnob) { + transposeCentTextKnob->sensitivity = 1.0 / 7200.0; + transposeCentTextKnob->lowSensitivity = 1.0 / 72000.0; + transposeCentTextKnob->wheelSensitivity = 1.0 / 7200.0; + } - addLabel(mixLeft0, mixTop8, labelWidth, labelHeight, uiTextSize, "Tuning"); + addLabel(mixLeft0, mixTop9, labelWidth, labelHeight, uiTextSize, "Tuning"); std::vector tuningItems{ "ET 12", "ET 5", "ET 10", "Just 5 Major", "Just 5 Minor", "Just 7", "MT Pythagorean", "MT 1/3 Comma", "MT 1/4 Comma", "Discrete 2", @@ -137,18 +149,18 @@ bool Editor::prepareUI() tuningItems.push_back("- Reserved " + std::to_string(idx) + " -"); } addOptionMenu( - mixLeft1, mixTop8, labelWidth, labelHeight, uiTextSize, ID::tuningType, tuningItems); + mixLeft1, mixTop9, labelWidth, labelHeight, uiTextSize, ID::tuningType, tuningItems); - addLabel(mixLeft0, mixTop9, labelWidth, labelHeight, uiTextSize, "Tuning Root [st.]"); + addLabel(mixLeft0, mixTop10, labelWidth, labelHeight, uiTextSize, "Tuning Root [st.]"); addTextKnob( - mixLeft1, mixTop9, labelWidth, labelHeight, uiTextSize, ID::tuningRootSemitone, + mixLeft1, mixTop10, labelWidth, labelHeight, uiTextSize, ID::tuningRootSemitone, Scales::tuningRootSemitone, false, 0); addCheckbox( - mixLeft0, mixTop10, labelWidth, labelHeight, uiTextSize, "Polyphonic", + mixLeft0, mixTop11, labelWidth, labelHeight, uiTextSize, "Polyphonic", ID::polyphonic); addCheckbox( - mixLeft1, mixTop10, labelWidth, labelHeight, uiTextSize, "Release", ID::release); + mixLeft1, mixTop11, labelWidth, labelHeight, uiTextSize, "Release", ID::release); // Filter. constexpr auto filterLabelWidth = 100.0f; @@ -211,9 +223,12 @@ bool Editor::prepareUI() constexpr auto waveformTop0 = top0 + 0 * labelY; constexpr auto waveformTop1 = waveformTop0 + 1 * labelY; constexpr auto waveformTop2 = waveformTop1 + barBoxWidth; - constexpr auto waveformTop3 = waveformTop2 + 2 * labelY; - constexpr auto waveformTop4 = waveformTop2 + 3 * labelY; - constexpr auto waveformTop5 = waveformTop2 + 4 * labelY; + constexpr auto waveformTop3 = waveformTop2 + 1 * labelY; + constexpr auto waveformTop4 = waveformTop2 + 2 * labelY; + constexpr auto waveformTop5 = waveformTop2 + 3 * labelY; + constexpr auto waveformTop6 = waveformTop2 + 4 * labelY; + constexpr auto waveformTop7 = waveformTop2 + 5 * labelY; + constexpr auto waveformTop8 = waveformTop2 + 6 * labelY; constexpr auto waveformLeft0 = left4; constexpr auto waveformLeft1 = left4 + labelWidth + 2 * margin; addGroupLabel( @@ -263,19 +278,31 @@ bool Editor::prepareUI() } addLabel( - waveformLeft0, waveformTop3, labelWidth, labelHeight, uiTextSize, "Osc. Sync."); + waveformLeft0, waveformTop4, labelWidth, labelHeight, uiTextSize, "Osc. Sync."); addTextKnob( - waveformLeft1, waveformTop3, labelWidth, labelHeight, uiTextSize, ID::oscSync, + waveformLeft1, waveformTop4, labelWidth, labelHeight, uiTextSize, ID::oscSync, Scales::defaultScale, false, 5); - addLabel(waveformLeft0, waveformTop4, labelWidth, labelHeight, uiTextSize, "FM Index"); + addLabel(waveformLeft0, waveformTop5, labelWidth, labelHeight, uiTextSize, "FM Index"); addTextKnob( - waveformLeft1, waveformTop4, labelWidth, labelHeight, uiTextSize, ID::fmIndex, + waveformLeft1, waveformTop5, labelWidth, labelHeight, uiTextSize, ID::fmIndex, Scales::fmIndex, false, 5); addLabel( - waveformLeft0, waveformTop5, labelWidth, labelHeight, uiTextSize, "Saturation [dB]"); + waveformLeft0, waveformTop6, labelWidth, labelHeight, uiTextSize, "Saturation [dB]"); addTextKnob( - waveformLeft1, waveformTop5, labelWidth, labelHeight, uiTextSize, ID::saturationGain, + waveformLeft1, waveformTop6, labelWidth, labelHeight, uiTextSize, ID::saturationGain, Scales::gain, true, 5); + addLabel( + waveformLeft0, waveformTop7, labelWidth, labelHeight, uiTextSize, + "Pulse Width / Bit Mask"); + addTextKnob( + waveformLeft1, waveformTop7, labelWidth, labelHeight, uiTextSize, ID::pulseWidthRatio, + Scales::defaultScale, false, 5); + addCheckbox( + waveformLeft0, waveformTop8, labelWidth, labelHeight, uiTextSize, + "Pulse Width Modulation", ID::pulseWidthModulation); + addCheckbox( + waveformLeft1, waveformTop8, labelWidth, labelHeight, uiTextSize, "Bitwise And", + ID::pulseWidthBitwiseAnd); // Arpeggio. constexpr auto arpTop0 = top0; @@ -297,7 +324,7 @@ bool Editor::prepareUI() arpLeft0, arpTop0, groupLabelWidth, labelHeight, uiTextSize, "Arpeggio", ID::arpeggioSwitch); addLabel(arpLeft0, arpTop1, labelWidth, labelHeight, uiTextSize, "Seed"); - auto seedTextKnob = addTextKnob( + auto seedTextKnob = addTextKnob( arpLeft1, arpTop1, labelWidth, labelHeight, uiTextSize, ID::seed, Scales::seed, false, 0); if (seedTextKnob) { @@ -325,23 +352,23 @@ bool Editor::prepareUI() std::vector arpeggioScaleItems{ "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", + "Church C (Major Scale)", + "Church D", + "Church E", + "Church F", + "Church G", + "Church A (Minor Scale)", + "Church B", + "Suspended 2", + "Suspended 4", + "Major 7", + "Minor 7", + "Major 7 Extended", + "Minor 7 Extended", + "Whole Tone 2", + "Whole Tone 3", + "Whole Tone 4", + "Blues", "Overtone 32", "The 42 Melody", "Overtone Odd 16", @@ -364,6 +391,12 @@ bool Editor::prepareUI() addTextKnob( arpLeft1, arpTop10, labelWidth, labelHeight, uiTextSize, ID::arpeggioOctave, Scales::arpeggioOctave, false, 0, 1); + addCheckbox( + arpLeft0, arpTop11, labelWidth, labelHeight, uiTextSize, "Start From Root", + ID::arpeggioStartFromRoot); + addCheckbox( + arpLeft1, arpTop11, labelWidth, labelHeight, uiTextSize, "Reset Modulation", + ID::arpeggioResetModulation); addLabel(arpLeft0, arpTop12, labelWidth, labelHeight, uiTextSize, "Random FM Index"); addTextKnob( @@ -382,7 +415,7 @@ bool Editor::prepareUI() unisonLeft0, unisonTop0, groupLabelWidth, labelHeight, uiTextSize, "Unison"); addLabel(unisonLeft0, unisonTop1, labelWidth, labelHeight, uiTextSize, "nVoice"); - addTextKnob( + addTextKnob( unisonLeft1, unisonTop1, labelWidth, labelHeight, uiTextSize, ID::unisonVoice, Scales::unisonVoice, false, 0, 1); addLabel(unisonLeft0, unisonTop2, labelWidth, labelHeight, uiTextSize, "Detune [cent]"); @@ -406,10 +439,10 @@ bool Editor::prepareUI() // Plugin name and randomize button. constexpr auto splashMargin = uiMargin; - constexpr auto splashTop = top0 + 18 * labelY - 2 * margin; - constexpr auto splashLeft = left4; + constexpr auto splashTop = top0 + 18 * labelY; + constexpr auto splashLeft = left8; const auto randomButtonTop = splashTop; - const auto randomButtonLeft = splashLeft + labelWidth + 2 * margin; + const auto randomButtonLeft = splashLeft; auto panicButton = new RandomizeButton( CRect( randomButtonLeft, randomButtonTop, randomButtonLeft + labelWidth, @@ -418,9 +451,9 @@ bool Editor::prepareUI() frame->addView(panicButton); addSplashScreen( - splashLeft, splashTop, labelWidth, splashHeight, splashMargin, splashMargin, - defaultWidth - 2 * splashMargin, defaultHeight - 2 * splashMargin, pluginNameTextSize, - "GlitchSprinkler", false); + splashLeft + labelWidth + 2 * margin, splashTop, labelWidth, splashHeight, + splashMargin, splashMargin, defaultWidth - 2 * splashMargin, + defaultHeight - 2 * splashMargin, pluginNameTextSize, "GlitchSprinkler", false); return true; } diff --git a/GlitchSprinkler/source/gui/splashdraw.cpp b/GlitchSprinkler/source/gui/splashdraw.cpp index 431a3a94..52bfef1b 100644 --- a/GlitchSprinkler/source/gui/splashdraw.cpp +++ b/GlitchSprinkler/source/gui/splashdraw.cpp @@ -85,12 +85,24 @@ Refer to the manual for a full list of shortcuts.)"; When `Decay to` is set to 0, and `Release` is on, sound won't stop after note-off. +When `Pulse Width / Bit Mask` is 1.0, and `Pulse +Width Modulation` is on, output becomes silent. + +When both of `Pulse Width Modulation` and +`Bitwise And` is on, aliasing may be noticeable. + When `Resonance` is near 1.0, `Polyphonic` is turned off, and fast sequence is played, amplitude may change for each note-on. Maximum voice number is 256. If CPU load is too -high, lower `Decay to`.)"; +high, lower `Decay to`. + +This synthesizer is using integer pitch tuning. +Because of this, higher pitches are increasingly +out of tune. `Discrete *` tunings are the most +clear, but they aren't compatible with +traditional tunings.)"; const float top0 = 100.0f; const float lineHeight = 20.0f; diff --git a/GlitchSprinkler/source/parameter.cpp b/GlitchSprinkler/source/parameter.cpp index 45027f27..12986761 100644 --- a/GlitchSprinkler/source/parameter.cpp +++ b/GlitchSprinkler/source/parameter.cpp @@ -51,7 +51,7 @@ LinearScale Scales::polynomialPointY(-0.5, 0.5); UIntScale Scales::transposeOctave(2 * transposeOctaveOffset); UIntScale Scales::transposeSemitone(2 * transposeSemitoneOffset); LinearScale Scales::transposeCent(-3600, 3600); -UIntScale Scales::tuningType(31); +UIntScale Scales::tuningType(63); UIntScale Scales::tuningRootSemitone(63); UIntScale Scales::arpeggioNotesPerBeat(15); diff --git a/GlitchSprinkler/source/parameter.hpp b/GlitchSprinkler/source/parameter.hpp index e79f4b69..702a0cc1 100644 --- a/GlitchSprinkler/source/parameter.hpp +++ b/GlitchSprinkler/source/parameter.hpp @@ -93,11 +93,16 @@ enum ID { bypass, outputGain, + safetyFilterMix, decayTargetGain, + decaySoftAttack, oscSync, fmIndex, saturationGain, + pulseWidthRatio, + pulseWidthModulation, + pulseWidthBitwiseAnd, randomizeFmIndex, @@ -132,6 +137,8 @@ enum ID { arpeggioScale, arpeggioPicthDriftCent, arpeggioOctave, + arpeggioStartFromRoot, + arpeggioResetModulation, unisonVoice, unisonDetuneCent, @@ -202,10 +209,15 @@ struct GlobalParameter : public ParameterInterface { value[ID::outputGain] = std::make_unique( Scales::gain.invmap(1.0), Scales::gain, "outputGain", Info::kCanAutomate); + value[ID::safetyFilterMix] = std::make_unique( + Scales::defaultScale.invmap(1.0), Scales::defaultScale, "safetyFilterMix", + Info::kCanAutomate); value[ID::decayTargetGain] = std::make_unique( Scales::decayTargetGain.invmapDB(-1.0), Scales::decayTargetGain, "decayTargetGain", Info::kCanAutomate); + value[ID::decaySoftAttack] = std::make_unique( + 1, Scales::boolScale, "decaySoftAttack", Info::kCanAutomate); value[ID::oscSync] = std::make_unique( Scales::defaultScale.invmap(1.0), Scales::defaultScale, "oscSync", Info::kCanAutomate); @@ -213,6 +225,13 @@ struct GlobalParameter : public ParameterInterface { 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::pulseWidthRatio] = std::make_unique( + Scales::defaultScale.invmap(0.0), Scales::defaultScale, "pulseWidthRatio", + Info::kCanAutomate); + value[ID::pulseWidthModulation] = std::make_unique( + 0, Scales::boolScale, "pulseWidthModulation", Info::kCanAutomate); + value[ID::pulseWidthBitwiseAnd] = std::make_unique( + 0, Scales::boolScale, "pulseWidthBitwiseAnd", Info::kCanAutomate); value[ID::randomizeFmIndex] = std::make_unique( Scales::randomizeFmIndex.invmap(0.0), Scales::randomizeFmIndex, "randomizeFmIndex", @@ -258,7 +277,7 @@ struct GlobalParameter : public ParameterInterface { Scales::defaultScale.invmap(0.0), Scales::defaultScale, "filterResonanceMod", Info::kCanAutomate); value[ID::filterNotchBaseOctave] = std::make_unique( - Scales::filterNotchBaseOctave.invmap(1.0), Scales::filterNotchBaseOctave, + Scales::filterNotchBaseOctave.invmap(0.0), Scales::filterNotchBaseOctave, "filterNotchBaseOctave", Info::kCanAutomate); value[ID::filterNotchModOctave] = std::make_unique( 0.0, Scales::filterNotchModOctave, "filterNotchModOctave", Info::kCanAutomate); @@ -297,6 +316,10 @@ struct GlobalParameter : public ParameterInterface { "arpeggioPicthDriftCent", Info::kCanAutomate); value[ID::arpeggioOctave] = std::make_unique( 1, Scales::arpeggioOctave, "arpeggioOctave", Info::kCanAutomate); + value[ID::arpeggioStartFromRoot] = std::make_unique( + 1, Scales::boolScale, "arpeggioStartFromRoot", Info::kCanAutomate); + value[ID::arpeggioResetModulation] = std::make_unique( + 0, Scales::boolScale, "arpeggioResetModulation", Info::kCanAutomate); value[ID::unisonVoice] = std::make_unique( 0, Scales::unisonVoice, "unisonVoice", Info::kCanAutomate); @@ -314,7 +337,7 @@ struct GlobalParameter : public ParameterInterface { for (size_t idx = 0; idx < nReservedParameter; ++idx) { auto indexStr = std::to_string(idx); value[ID::reservedParameter0 + idx] = std::make_unique( - Scales::defaultScale.invmap(1.0), Scales::defaultScale, + Scales::defaultScale.invmap(0.0), Scales::defaultScale, ("reservedParameter" + indexStr).c_str(), Info::kIsHidden); }