diff --git a/GlitchSprinkler/source/dsp/dspcore.cpp b/GlitchSprinkler/source/dsp/dspcore.cpp index 9c1df4d5..ae192656 100644 --- a/GlitchSprinkler/source/dsp/dspcore.cpp +++ b/GlitchSprinkler/source/dsp/dspcore.cpp @@ -37,7 +37,7 @@ void DSPCore::setup(double sampleRate_) SmootherCommon::setTime(double(0.2)); - terminationLength = uint_fast32_t(double(0.002) * sampleRate); + terminationLength = int_fast32_t(double(0.002) * sampleRate); lowpassInterpRate = sampleRate / double(48000 * 64); softAttackEmaRatio = EMAFilter::cutoffToP(double(50) / sampleRate); @@ -77,7 +77,23 @@ void DSPCore::setup(double sampleRate_) arpeggioLoopLength = std::numeric_limits::max(); \ } \ \ + arpeggioSwitch = pv[ID::arpeggioSwitch]->getInt(); \ + filterSwitch = pv[ID::filterSwitch]->getInt(); \ + softEnvelopeSwitch = pv[ID::softEnvelopeSwitch]->getInt(); \ + pwmSwitch = pv[ID::pulseWidthModulation]->getInt(); \ + pwmBidrection = pv[ID::pulseWidthModBidirectionSwitch]->getInt(); \ + bitmaskSwitch = pv[ID::pulseWidthBitwiseAnd]->getInt(); \ pwmRatio = double(1) - pv[ID::pulseWidthRatio]->getDouble(); \ + \ + const auto pulseWidthModRate = pv[ID::pulseWidthModRate]->getInt(); \ + if (pulseWidthModRate < pwmOneCyclePoint) { \ + pwmChangeCycle = 1; \ + pwmAmount = pwmOneCyclePoint - pulseWidthModRate + 1; \ + } else { \ + pwmChangeCycle = 1 + pulseWidthModRate - pwmOneCyclePoint; \ + pwmAmount = 1; \ + } \ + \ safetyFilterMix.METHOD(pv[ID::safetyFilterMix]->getDouble()); \ outputGain.METHOD(pv[ID::outputGain]->getDouble()); \ \ @@ -118,6 +134,7 @@ void Voice::reset() pwmPoint = 0; pwmBitMask = 0; pwmDirection = 1; + pwmChangeCounter = 1; phasePeriod = 0; phaseCounter = 0; oscSync = double(1); @@ -183,7 +200,7 @@ std::array Voice::processFrame() const auto &pv = core.param.value; // Arpeggio. - if (pv[ID::arpeggioSwitch]->getInt()) { + if (core.arpeggioSwitch) { if (arpeggioTimer < arpeggioTie * core.arpeggioNoteDuration) { ++arpeggioTimer; } else if (state == State::active) { @@ -203,7 +220,10 @@ std::array Voice::processFrame() (state == State::terminate && phaseCounter == 0) || (state == State::release && lastGain <= epsf32) || (terminationCounter > 0)) { - if (terminationCounter < core.terminationLength && pv[ID::filterSwitch]->getInt()) { + if ( + terminationCounter < core.terminationLength + && (core.filterSwitch || core.softEnvelopeSwitch)) + { ++terminationCounter; } else { terminationCounter = core.terminationLength; @@ -231,7 +251,7 @@ std::array Voice::processFrame() sig = std::clamp(saturationGain * sig, double(-1), double(1)); // Lowpass. - if (pv[ID::filterSwitch]->getInt()) { + if (core.filterSwitch) { sig = lowpass.process( sig, core.lowpassInterpRate, cutoffBase + cutoffMod * filterDecayRatio, resonanceBase + resonanceMod * filterDecayRatio, @@ -250,21 +270,32 @@ std::array Voice::processFrame() 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; + if (pwmChangeCounter <= 0) { + pwmChangeCounter = core.pwmChangeCycle; + + if (core.pwmSwitch) { + pwmPoint = std::clamp( + pwmPoint + pwmDirection, int_fast32_t(0), int_fast32_t(phasePeriod)); + + if (pwmPoint <= pwmLower) { + if (core.pwmBidrection) { + pwmDirection = core.pwmAmount; + } else { + pwmPoint = phasePeriod; + } + } else if (pwmPoint >= phasePeriod) { + pwmDirection = -core.pwmAmount; + } + } else { + pwmPoint = int_fast32_t(phasePeriod * core.pwmRatio); } + + pwmBitMask + = core.bitmaskSwitch ? pwmPoint : std::numeric_limits::max(); } else { - pwmPoint = uint_fast32_t(phasePeriod * core.pwmRatio); + --pwmChangeCounter; } - pwmBitMask = pv[ID::pulseWidthBitwiseAnd]->getInt() - ? pwmPoint - : std::numeric_limits::max(); - if (scheduleUpdateNote) { scheduleUpdateNote = false; if (state == State::active) updateNote(); @@ -369,8 +400,8 @@ 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.pwmChangeCounter = pwmChangeCycle; + vc.decayEmaRatio = softEnvelopeSwitch ? softAttackEmaRatio : double(1); vc.decayEmaValue = 0; vc.lowpass.reset(); vc.updateNote(); @@ -477,7 +508,7 @@ void Voice::updateNote() if (core.activeNote.empty()) return; - const auto useArpeggiator = pv[ID::arpeggioSwitch]->getInt(); + const auto useArpeggiator = core.arpeggioSwitch; if (!core.isPolyphonic) { std::uniform_int_distribution indexDist{0, core.activeNote.size() - 1}; @@ -617,17 +648,16 @@ void Voice::updateNote() } polynomialCoefficients = core.polynomial.coefficients; - pwmLower = uint_fast32_t(phasePeriod * core.pwmRatio); + pwmLower = int_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; + pwmPoint = core.pwmSwitch ? phasePeriod : pwmLower; + pwmDirection = core.pwmAmount; - pwmBitMask = pv[ID::pulseWidthBitwiseAnd]->getInt() - ? pwmPoint - : std::numeric_limits::max(); + pwmBitMask + = core.bitmaskSwitch ? pwmPoint : std::numeric_limits::max(); } oscSync = pv[ID::oscSync]->getDouble(); @@ -652,7 +682,7 @@ void Voice::updateNote() lastGain = decayGain; // Filter. - if (pv[ID::filterSwitch]->getInt()) { + if (core.filterSwitch) { const auto cutoffBaseOctave = pv[ID::filterCutoffBaseOctave]->getDouble(); const auto cutoffKeyFollow = pv[ID::filterCutoffKeyFollow]->getDouble(); const auto baseFreq diff --git a/GlitchSprinkler/source/dsp/dspcore.hpp b/GlitchSprinkler/source/dsp/dspcore.hpp index fee85495..b9eb76ec 100644 --- a/GlitchSprinkler/source/dsp/dspcore.hpp +++ b/GlitchSprinkler/source/dsp/dspcore.hpp @@ -65,18 +65,19 @@ class Voice { std::minstd_rand rngArpeggio{0}; - uint_fast32_t arpeggioTie = 1; - uint_fast32_t arpeggioTimer = 0; - uint_fast32_t arpeggioLoopCounter = 0; - uint_fast32_t terminationCounter = 0; + int_fast32_t arpeggioTie = 1; + int_fast32_t arpeggioTimer = 0; + int_fast32_t arpeggioLoopCounter = 0; + int_fast32_t terminationCounter = 0; bool scheduleUpdateNote = false; - uint_fast32_t pwmLower = 0; - uint_fast32_t pwmPoint = 0; + int_fast32_t pwmLower = 0; + int_fast32_t pwmPoint = 0; uint_fast32_t pwmBitMask = 0; int_fast32_t pwmDirection = 1; - uint_fast32_t phasePeriod = 0; - uint_fast32_t phaseCounter = 0; + int_fast32_t pwmChangeCounter = 1; + int_fast32_t phasePeriod = 0; + int_fast32_t phaseCounter = 0; double oscSync = double(1); double fmIndex = double(0); double saturationGain = double(1); @@ -121,13 +122,21 @@ class DSPCore { bool isPolyphonic = true; Voice::State noteOffState = Voice::State::terminate; - uint_fast32_t arpeggioNoteDuration = std::numeric_limits::max(); - uint_fast32_t arpeggioLoopLength = std::numeric_limits::max(); - uint_fast32_t terminationLength = 0; + int_fast32_t arpeggioNoteDuration = std::numeric_limits::max(); + int_fast32_t arpeggioLoopLength = std::numeric_limits::max(); + int_fast32_t terminationLength = 0; + bool arpeggioSwitch = false; + bool filterSwitch = false; + bool softEnvelopeSwitch = false; + bool pwmSwitch = false; + bool pwmBidrection = false; + bool bitmaskSwitch = false; double lowpassInterpRate = double(1) / double(64); double softAttackEmaRatio = double(1); double pwmRatio = double(1); + int_fast32_t pwmChangeCycle = 1; + int_fast32_t pwmAmount = 1; DecibelScale velocityMap{-60, 0, true}; ExpSmoother safetyFilterMix; ExpSmoother outputGain; diff --git a/GlitchSprinkler/source/editor.cpp b/GlitchSprinkler/source/editor.cpp index ba1c2923..96b03c2a 100644 --- a/GlitchSprinkler/source/editor.cpp +++ b/GlitchSprinkler/source/editor.cpp @@ -36,6 +36,7 @@ constexpr float knobY = knobWidth + labelHeight + 2 * margin; constexpr float labelY = labelHeight + 2 * margin; constexpr float labelWidth = 2 * knobWidth; constexpr float groupLabelWidth = 2 * labelWidth + 2 * margin; +constexpr float labelWidthOneThird = int(groupLabelWidth / 3); constexpr float splashWidth = int(labelWidth * 3 / 2) + 2 * margin; constexpr float splashHeight = int(labelHeight + 2 * margin); @@ -118,8 +119,14 @@ bool Editor::prepareUI() mixLeft1, mixTop4, labelWidth, labelHeight, uiTextSize, ID::decayTargetGain, Scales::decayTargetGain, true, 5); addCheckbox( - mixLeft0, mixTop5, labelWidth, labelHeight, uiTextSize, "Soft Attack", - ID::decaySoftAttack); + mixLeft0, mixTop5, labelWidthOneThird, labelHeight, uiTextSize, "Polyphonic", + ID::polyphonic); + addCheckbox( + mixLeft0 + labelWidthOneThird * 1, mixTop5, labelWidthOneThird, labelHeight, + uiTextSize, "Release", ID::release); + addCheckbox( + mixLeft0 + labelWidthOneThird * 2, mixTop5, labelWidthOneThird, labelHeight, + uiTextSize, "Soft Envelope", ID::softEnvelopeSwitch); addLabel(mixLeft0, mixTop6, labelWidth, labelHeight, uiTextSize, "Octave"); addTextKnob( @@ -156,12 +163,6 @@ bool Editor::prepareUI() mixLeft1, mixTop10, labelWidth, labelHeight, uiTextSize, ID::tuningRootSemitone, Scales::tuningRootSemitone, false, 0); - addCheckbox( - mixLeft0, mixTop11, labelWidth, labelHeight, uiTextSize, "Polyphonic", - ID::polyphonic); - addCheckbox( - mixLeft1, mixTop11, labelWidth, labelHeight, uiTextSize, "Release", ID::release); - // Filter. constexpr auto filterLabelWidth = 100.0f; constexpr auto filterTop0 = mixTop11 + labelY; @@ -278,31 +279,40 @@ bool Editor::prepareUI() } addLabel( - waveformLeft0, waveformTop4, labelWidth, labelHeight, uiTextSize, "Osc. Sync."); + waveformLeft0, waveformTop3, labelWidth, labelHeight, uiTextSize, "Osc. Sync."); addTextKnob( - waveformLeft1, waveformTop4, labelWidth, labelHeight, uiTextSize, ID::oscSync, + waveformLeft1, waveformTop3, labelWidth, labelHeight, uiTextSize, ID::oscSync, Scales::defaultScale, false, 5); - addLabel(waveformLeft0, waveformTop5, labelWidth, labelHeight, uiTextSize, "FM Index"); + addLabel(waveformLeft0, waveformTop4, labelWidth, labelHeight, uiTextSize, "FM Index"); addTextKnob( - waveformLeft1, waveformTop5, labelWidth, labelHeight, uiTextSize, ID::fmIndex, + waveformLeft1, waveformTop4, labelWidth, labelHeight, uiTextSize, ID::fmIndex, Scales::fmIndex, false, 5); addLabel( - waveformLeft0, waveformTop6, labelWidth, labelHeight, uiTextSize, "Saturation [dB]"); + waveformLeft0, waveformTop5, labelWidth, labelHeight, uiTextSize, "Saturation [dB]"); addTextKnob( - waveformLeft1, waveformTop6, labelWidth, labelHeight, uiTextSize, ID::saturationGain, + waveformLeft1, waveformTop5, labelWidth, labelHeight, uiTextSize, ID::saturationGain, Scales::gain, true, 5); addLabel( - waveformLeft0, waveformTop7, labelWidth, labelHeight, uiTextSize, + waveformLeft0, waveformTop6, labelWidth, labelHeight, uiTextSize, "Pulse Width / Bit Mask"); addTextKnob( - waveformLeft1, waveformTop7, labelWidth, labelHeight, uiTextSize, ID::pulseWidthRatio, + waveformLeft1, waveformTop6, labelWidth, labelHeight, uiTextSize, ID::pulseWidthRatio, Scales::defaultScale, false, 5); + addLabel( + waveformLeft0, waveformTop7, labelWidth, labelHeight, uiTextSize, "Modulation Rate"); + addTextKnob( + waveformLeft1, waveformTop7, labelWidth, labelHeight, uiTextSize, + ID::pulseWidthModRate, Scales::pulseWidthModRate, false, 0, -pwmOneCyclePoint); + addCheckbox( - waveformLeft0, waveformTop8, labelWidth, labelHeight, uiTextSize, - "Pulse Width Modulation", ID::pulseWidthModulation); + waveformLeft0, waveformTop8, labelWidthOneThird, labelHeight, uiTextSize, "PWM", + ID::pulseWidthModulation); + addCheckbox( + waveformLeft0 + labelWidthOneThird * 1, waveformTop8, labelWidthOneThird, labelHeight, + uiTextSize, "Bidirection", ID::pulseWidthModBidirectionSwitch); addCheckbox( - waveformLeft1, waveformTop8, labelWidth, labelHeight, uiTextSize, "Bitwise And", - ID::pulseWidthBitwiseAnd); + waveformLeft0 + labelWidthOneThird * 2, waveformTop8, labelWidthOneThird, labelHeight, + uiTextSize, "Bitwise And", ID::pulseWidthBitwiseAnd); // Arpeggio. constexpr auto arpTop0 = top0; diff --git a/GlitchSprinkler/source/parameter.cpp b/GlitchSprinkler/source/parameter.cpp index 12986761..15073541 100644 --- a/GlitchSprinkler/source/parameter.cpp +++ b/GlitchSprinkler/source/parameter.cpp @@ -38,6 +38,7 @@ DecibelScale Scales::gain(-100.0, 60.0, true); DecibelScale Scales::decayTargetGain(-150.0, 0.0, false); DecibelScale Scales::fmIndex(-60.0, 40.0, true); +UIntScale Scales::pulseWidthModRate(511); LinearScale Scales::randomizeFmIndex(0.0, 4.0); LinearScale Scales::filterDecayRatio(-10.0, 10.0); @@ -58,7 +59,7 @@ UIntScale Scales::arpeggioNotesPerBeat(15); UIntScale Scales::arpeggioLoopLengthInBeat(256); UIntScale Scales::arpeggioDurationVariation(15); UIntScale Scales::arpeggioScale(255); -LinearScale Scales::arpeggioPicthDriftCent(0, 100); +LinearScale Scales::arpeggioPicthDriftCent(0, 200); UIntScale Scales::arpeggioOctave(7); UIntScale Scales::unisonVoice(255); diff --git a/GlitchSprinkler/source/parameter.hpp b/GlitchSprinkler/source/parameter.hpp index 702a0cc1..e4d603ec 100644 --- a/GlitchSprinkler/source/parameter.hpp +++ b/GlitchSprinkler/source/parameter.hpp @@ -31,7 +31,8 @@ #include "../../common/value.hpp" #endif -static constexpr size_t nPolyOscControl = 11; +constexpr size_t nPolyOscControl = 11; +constexpr int_fast32_t pwmOneCyclePoint = 256; constexpr size_t nReservedParameter = 256; constexpr size_t nReservedGuiParameter = 64; @@ -96,12 +97,14 @@ enum ID { safetyFilterMix, decayTargetGain, - decaySoftAttack, + softEnvelopeSwitch, oscSync, fmIndex, saturationGain, pulseWidthRatio, + pulseWidthModRate, pulseWidthModulation, + pulseWidthModBidirectionSwitch, pulseWidthBitwiseAnd, randomizeFmIndex, @@ -164,6 +167,7 @@ struct Scales { static SomeDSP::DecibelScale decayTargetGain; static SomeDSP::DecibelScale fmIndex; + static SomeDSP::UIntScale pulseWidthModRate; static SomeDSP::LinearScale randomizeFmIndex; static SomeDSP::LinearScale filterDecayRatio; @@ -216,8 +220,8 @@ struct GlobalParameter : public ParameterInterface { 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::softEnvelopeSwitch] = std::make_unique( + 0, Scales::boolScale, "softEnvelopeSwitch", Info::kCanAutomate); value[ID::oscSync] = std::make_unique( Scales::defaultScale.invmap(1.0), Scales::defaultScale, "oscSync", Info::kCanAutomate); @@ -228,8 +232,13 @@ struct GlobalParameter : public ParameterInterface { value[ID::pulseWidthRatio] = std::make_unique( Scales::defaultScale.invmap(0.0), Scales::defaultScale, "pulseWidthRatio", Info::kCanAutomate); + value[ID::pulseWidthModRate] = std::make_unique( + pwmOneCyclePoint, Scales::pulseWidthModRate, "pulseWidthModRate", + Info::kCanAutomate); value[ID::pulseWidthModulation] = std::make_unique( 0, Scales::boolScale, "pulseWidthModulation", Info::kCanAutomate); + value[ID::pulseWidthModBidirectionSwitch] = std::make_unique( + 1, Scales::boolScale, "pulseWidthModBidirectionSwitch", Info::kCanAutomate); value[ID::pulseWidthBitwiseAnd] = std::make_unique( 0, Scales::boolScale, "pulseWidthBitwiseAnd", Info::kCanAutomate);