diff --git a/CMakeLists.txt b/CMakeLists.txt index fd94bc7..5334767 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,17 @@ target_sources(Monostep PRIVATE target_include_directories(Monostep PRIVATE Source) +# Regenerate BuildStamp.h on every build so the UI's build date is always fresh. +set(MONOSTEP_BUILD_STAMP_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/BuildStamp.h") +add_custom_target(MonostepBuildStamp ALL + COMMAND ${CMAKE_COMMAND} + -DMONOSTEP_BUILD_STAMP_HEADER="${MONOSTEP_BUILD_STAMP_HEADER}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WriteBuildStamp.cmake" + COMMENT "Writing build stamp" +) +add_dependencies(Monostep MonostepBuildStamp) +target_include_directories(Monostep PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated") + target_compile_definitions(Monostep PRIVATE JUCE_WEB_BROWSER=0 JUCE_VST3_CAN_REPLACE_VST2=0 diff --git a/Source/BuildInfo.h b/Source/BuildInfo.h index 1946af8..3798e39 100644 --- a/Source/BuildInfo.h +++ b/Source/BuildInfo.h @@ -1,6 +1,7 @@ #pragma once #include +#include "BuildStamp.h" namespace monostep { @@ -19,6 +20,9 @@ inline juce::String gitInfo() inline juce::String buildDateString() { +#if defined (MONOSTEP_BUILD_DATE) + return juce::String (MONOSTEP_BUILD_DATE); +#else static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; @@ -36,6 +40,7 @@ inline juce::String buildDateString() const int year = d.substring (7).getIntValue(); return juce::String::formatted ("%04d-%02d-%02d__%s", year, month, day, __TIME__); +#endif } inline juce::String footerText() diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index b81c8f9..fc6d33c 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -504,6 +504,14 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef) processor.randomizeSeqSettings(); if (randomizeToggles[RandomFx].getToggleState()) processor.randomizeFx(); + if (randomizeToggles[RandomAccent].getToggleState()) + processor.randomizeAccent(); + if (randomizeToggles[RandomSlide].getToggleState()) + processor.randomizeSlide(); + if (randomizeToggles[RandomFine].getToggleState()) + processor.randomizeFine(); + if (randomizeToggles[RandomRate].getToggleState()) + processor.randomizeRate(); }; addAndMakeVisible (randomizeButton); @@ -513,7 +521,8 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef) randomizeCaption.setColour (juce::Label::textColourId, colours::textDim); addAndMakeVisible (randomizeCaption); - static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX" }; + static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX", + "Accent", "Slide", "Fine", "Rate" }; for (int i = 0; i < RandomGroupCount; ++i) { @@ -590,41 +599,41 @@ void StepPanel::resized() const int w = getWidth(); titleLabel.setBounds (0, 4, w, 16); - noteLabel.setBounds (0, 22, w, 28); + noteLabel.setBounds (0, 22, w, 26); gateLabel.setBounds (0, 50, w, 14); const int knobX = (w - 74) / 2; - const int knobY = 68; + const int knobY = 66; - fineKnob.setBounds (knobX, knobY, 74, 94); + fineKnob.setBounds (knobX, knobY, 74, 84); - const int btnY = knobY + 102; + const int btnY = knobY + 84 + 6; prevButton.setBounds (knobX - 32, btnY, 24, 24); nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24); const int shiftW = 22; - shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26); - clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26); - shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26); + shiftLeftButton.setBounds (10, btnY + 40, shiftW, 24); + clearButton.setBounds (10 + shiftW + 4, btnY + 40, w - 20 - shiftW * 2 - 8, 24); + shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 40, shiftW, 24); - toolsCaption.setBounds (10, btnY + 86, w - 20, 14); - presetCombo.setBounds (10, btnY + 102, w - 20, 26); - randomizeButton.setBounds (10, btnY + 134, w - 20, 26); - randomizeCaption.setBounds (10, btnY + 166, w - 20, 12); + toolsCaption.setBounds (10, btnY + 70, w - 20, 12); + presetCombo.setBounds (10, btnY + 84, w - 20, 24); + randomizeButton.setBounds (10, btnY + 112, w - 20, 24); + randomizeCaption.setBounds (10, btnY + 140, w - 20, 12); const int colW = (w - 20 - 4) / 2; - const int rowH = 20; + const int rowH = 18; for (int i = 0; i < RandomGroupCount; ++i) { - const int col = i / 3; - const int row = i % 3; - randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH); + const int col = i % 2; + const int row = i / 2; + randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 154 + row * (rowH + 2), colW, rowH); } const int retrigW = 100; - retrigToggle.setBounds (10, btnY + 252, retrigW, 26); - resetFineButton.setBounds (10 + retrigW + 4, btnY + 252, w - 20 - retrigW - 4, 26); + retrigToggle.setBounds (10, btnY + 254, retrigW, 24); + resetFineButton.setBounds (10 + retrigW + 4, btnY + 254, w - 20 - retrigW - 4, 24); } //============================================================================== @@ -659,10 +668,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess driveKnob ("Drive"), ringKnob ("Ring"), delayTimeKnob ("Time"), - delayStrengthKnob ("Strength"), + delayFeedbackKnob ("Feedback"), + delayDryWetKnob ("Dry/Wet"), reverbRoomKnob ("Room"), - reverbStrengthKnob ("Strength"), + reverbStrengthKnob ("Level"), reverbDiffusionKnob ("Diff"), + reverbDryWetKnob ("Dry/Wet"), accentKnob ("Accent"), rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }), octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }), @@ -770,10 +781,12 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess const bool sync = processorRef.getAPVTS().getRawParameterValue ("delaySync")->load() >= 0.5f; return monostep::FxProcessor::delayTimeLabel (v, sync, 120.0); }); - addKnob (delayStrengthKnob, "delayStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); + addKnob (delayFeedbackKnob, "delayStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); + addKnob (delayDryWetKnob, "delayDryWet", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); addKnob (reverbRoomKnob, "reverbRoom", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); addKnob (reverbStrengthKnob, "reverbStrength", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); addKnob (reverbDiffusionKnob, "reverbDiffusion", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); + addKnob (reverbDryWetKnob, "reverbDryWet", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); delaySyncToggle.setButtonText ("Sync"); @@ -948,10 +961,10 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g) drawFrame (glideKnob, accentKnob, 6.0f); drawPill (glideKnob, accentKnob, "NOTE"); - drawFrame (driveKnob, reverbDiffusionKnob, 6.0f); + drawFrame (driveKnob, reverbDryWetKnob, 6.0f); drawPill (driveKnob, ringKnob, "FX"); drawPill (delayTimeKnob, delaySyncToggle, "DELAY"); - drawPill (reverbRoomKnob, reverbDiffusionKnob, "REVERB"); + drawPill (reverbRoomKnob, reverbDryWetKnob, "REVERB"); drawFrame (filterTypeBox, resKnob, 6.0f, 6.0f); drawPill (filterTypeBox, resKnob, "FILTER", 6.0f); @@ -1027,7 +1040,7 @@ void MonostepAudioProcessorEditor::resized() const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs // Both rows are centered over the full bottom bar width. - const int blockW = oscColW + 12 * step + 104; // osc col + FX (8 widgets + subgroup gaps) + const int blockW = oscColW + 15 * step + 152; // osc col + FX (9 widgets + sync toggle + subgroup gaps) const int oscX = pad + ((w - 2 * pad) - blockW) / 2 + oscColW; const int row1Y = bottomY + 24; const int row2Y = row1Y + knobH + 34; // row 1 + a caption band @@ -1053,9 +1066,9 @@ void MonostepAudioProcessorEditor::resized() const int fxX = oscX + 5 * step + 48; placeRow (row1Y, fxX, { &driveKnob, &ringKnob }); - placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayStrengthKnob }); - placeRow (row1Y, fxX + 5 * step + 48, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob }); - delaySyncToggle.setBounds (fxX + 4 * step + 24, row1Y + (knobH - 24) / 2, knobW, 24); + placeRow (row1Y, fxX + 2 * step + 24, { &delayTimeKnob, &delayFeedbackKnob, &delayDryWetKnob }); + placeRow (row1Y, fxX + 6 * step + 46, { &reverbRoomKnob, &reverbStrengthKnob, &reverbDiffusionKnob, &reverbDryWetKnob }); + delaySyncToggle.setBounds (fxX + 5 * step + 24, row1Y + (knobH - 24) / 2, knobW, 24); // Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env. // The three sections are centered as one block with equal frame gaps. diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index d6516b0..88b2462 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -153,6 +153,10 @@ private: RandomEnv, RandomSeq, RandomFx, + RandomAccent, + RandomSlide, + RandomFine, + RandomRate, RandomGroupCount }; @@ -194,7 +198,7 @@ private: ContentComponent content; - static constexpr int baseWidth = 1280; + static constexpr int baseWidth = 1360; static constexpr int baseHeight = 780; float zoomFactor = 1.0f; @@ -230,10 +234,12 @@ private: Knob driveKnob; Knob ringKnob; Knob delayTimeKnob; - Knob delayStrengthKnob; + Knob delayFeedbackKnob; + Knob delayDryWetKnob; Knob reverbRoomKnob; Knob reverbStrengthKnob; Knob reverbDiffusionKnob; + Knob reverbDryWetKnob; juce::ToggleButton delaySyncToggle; std::unique_ptr delaySyncAttachment; Knob accentKnob; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index b2f4e5f..350f49d 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -14,8 +14,8 @@ static const char* paramId (int index) "drive", "ringmod", "accent", "fine1", "filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt", "seqRetrig", - "delayTime", "delayStrength", "delaySync", - "reverbRoom", "reverbStrength", "reverbDiffusion" + "delayTime", "delayStrength", "delaySync", "delayDryWet", + "reverbRoom", "reverbStrength", "reverbDiffusion", "reverbDryWet" }; return ids[index]; } @@ -28,8 +28,8 @@ enum ParamIndex pDrive, pRingMod, pAccent, pFine1, pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt, pSeqRetrig, - pDelayTime, pDelayStrength, pDelaySync, - pReverbRoom, pReverbStrength, pReverbDiffusion, + pDelayTime, pDelayStrength, pDelaySync, pDelayDryWet, + pReverbRoom, pReverbStrength, pReverbDiffusion, pReverbDryWet, numParams }; @@ -72,9 +72,11 @@ MonostepAudioProcessor::MonostepAudioProcessor() delayTimeParam = apvts.getRawParameterValue (paramId (pDelayTime)); delayStrengthParam = apvts.getRawParameterValue (paramId (pDelayStrength)); delaySyncParam = apvts.getRawParameterValue (paramId (pDelaySync)); + delayDryWetParam = apvts.getRawParameterValue (paramId (pDelayDryWet)); reverbRoomParam = apvts.getRawParameterValue (paramId (pReverbRoom)); reverbStrengthParam = apvts.getRawParameterValue (paramId (pReverbStrength)); reverbDiffusionParam = apvts.getRawParameterValue (paramId (pReverbDiffusion)); + reverbDryWetParam = apvts.getRawParameterValue (paramId (pReverbDryWet)); setDefaultPattern(); } @@ -186,20 +188,26 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea "Delay Time", juce::NormalisableRange (0.0f, 1.0f), 0.35f)); layout.add (std::make_unique (paramId (pDelayStrength), - "Delay Strength", juce::NormalisableRange (0.0f, 1.0f), 0.0f)); + "Delay Feedback", juce::NormalisableRange (0.0f, 1.0f), 0.5f)); layout.add (std::make_unique (paramId (pDelaySync), "Delay Sync", true)); + layout.add (std::make_unique (paramId (pDelayDryWet), + "Delay Dry/Wet", juce::NormalisableRange (0.0f, 1.0f), 0.25f)); + layout.add (std::make_unique (paramId (pReverbRoom), "Reverb Roomsize", juce::NormalisableRange (0.0f, 1.0f), 0.5f)); layout.add (std::make_unique (paramId (pReverbStrength), - "Reverb Strength", juce::NormalisableRange (0.0f, 1.0f), 0.0f)); + "Reverb Strength", juce::NormalisableRange (0.0f, 1.0f), 1.0f)); layout.add (std::make_unique (paramId (pReverbDiffusion), "Reverb Diffusion", juce::NormalisableRange (0.0f, 1.0f), 0.6f)); + layout.add (std::make_unique (paramId (pReverbDryWet), + "Reverb Dry/Wet", juce::NormalisableRange (0.0f, 1.0f), 0.25f)); + return layout; } @@ -283,31 +291,39 @@ void MonostepAudioProcessor::applyPreset (int index) float glide; float gateLen; float swing; int rate; int root; int octave; float drive; float ring; float accent; float fAttack; float fDecay; float fSustain; float fRelease; float fAmt; + float delayTime = 0.35f; // only audible when delayWet > 0 + float delayFeedback = 0.45f; + bool delaySync = true; + float delayWet = 0.0f; + float reverbRoom = 0.5f; // only audible when reverbWet > 0 + float reverbLevel = 0.6f; + float reverbDiff = 0.6f; + float reverbWet = 0.0f; }; static const Preset presets[] = { // --- gritty / glidy ----------------------------------------------------- - { 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f, 0.02f, 0.30f, 0.60f, 0.25f, 1.5f }, // Wet Tape + { 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f, 0.02f, 0.30f, 0.60f, 0.25f, 1.5f, 0.21f, 0.55f, true, 0.50f, 0.60f, 0.60f, 0.50f, 0.15f }, // Wet Tape { 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f, 0.005f, 0.35f, 0.10f, 0.10f, 3.0f }, // Acid Wash { 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f, 0.005f, 0.15f, 0.30f, 0.15f, 0.5f }, // Tin Can { 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f, 0.03f, 0.25f, 0.50f, 0.40f, 1.0f }, // Bent Toy { 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f, 0.005f, 0.20f, 0.40f, 0.15f, 1.5f }, // Broken Radio - { 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f, 0.01f, 0.30f, 0.70f, 0.30f, 1.0f }, // Neon Arcade - { 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.40f, 0.50f, 0.20f, -1.5f }, // Glitch Garden - { 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f, 0.01f, 0.25f, 0.60f, 0.20f, 2.0f }, // Robo Funk - { 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f, 0.02f, 0.30f, 0.50f, 0.30f, -0.5f }, // Lo-Fi Noir + { 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f, 0.01f, 0.30f, 0.70f, 0.30f, 1.0f, 0.50f, 0.45f, true, 0.30f, 0.50f, 0.50f, 0.60f, 0.30f }, // Neon Arcade + { 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.40f, 0.50f, 0.20f, -1.5f, 0.36f, 0.70f, true, 0.50f, 0.50f, 0.60f, 0.60f, 0.00f }, // Glitch Garden + { 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f, 0.01f, 0.25f, 0.60f, 0.20f, 2.0f, 0.36f, 0.50f, true, 0.40f, 0.50f, 0.60f, 0.60f, 0.00f }, // Robo Funk + { 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f, 0.02f, 0.30f, 0.50f, 0.30f, -0.5f, 0.21f, 0.40f, true, 0.20f, 0.60f, 0.50f, 0.50f, 0.25f }, // Lo-Fi Noir { 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f, 0.01f, 0.30f, 0.20f, 0.30f, 3.5f }, // Spectral Sweep - { 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f, 0.02f, 0.40f, 0.80f, 0.30f, 0.5f }, // Plasma Organ - { 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f, 0.05f, 0.30f, 0.50f, 0.40f, -2.0f }, // Underwater FM + { 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f, 0.02f, 0.40f, 0.80f, 0.30f, 0.5f, 0.35f, 0.45f, true, 0.00f, 0.70f, 0.80f, 0.70f, 0.40f }, // Plasma Organ + { 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f, 0.05f, 0.30f, 0.50f, 0.40f, -2.0f, 0.64f, 0.50f, true, 0.35f, 0.90f, 0.70f, 0.90f, 0.35f }, // Underwater FM // --- clean / no glide / no drive --------------------------------------- { 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f, 0.005f, 0.20f, 0.30f, 0.40f, 0.3f }, // Glass Bells - { 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f, 0.50f, 1.00f, 0.70f, 1.20f, 1.0f }, // Warm Pad + { 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f, 0.50f, 1.00f, 0.70f, 1.20f, 1.0f, 0.35f, 0.45f, true, 0.00f, 0.80f, 0.90f, 0.80f, 0.45f }, // Warm Pad { 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.25f, 0.40f, 0.20f, 1.5f }, // Pulse Groove { 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f, 0.002f, 0.30f, 0.20f, 0.25f, 2.5f }, // Steel Pluck { 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f, 0.01f, 0.30f, 0.60f, 0.30f, -1.0f }, // Sub Rumble - { 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f, 0.01f, 0.30f, 0.50f, 0.30f, 1.0f }, // Tape Bounce + { 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f, 0.01f, 0.30f, 0.50f, 0.30f, 1.0f, 0.64f, 0.60f, true, 0.45f, 0.50f, 0.50f, 0.50f, 0.20f }, // Tape Bounce }; const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)]; @@ -337,6 +353,15 @@ void MonostepAudioProcessor::applyPreset (int index) applyParamValue ("filterSustain", pr.fSustain); applyParamValue ("filterRelease", pr.fRelease); applyParamValue ("filterEnvAmt", pr.fAmt); + + applyParamValue ("delayTime", pr.delayTime); + applyParamValue ("delayStrength", pr.delayFeedback); + applyParamValue ("delaySync", pr.delaySync ? 1.0f : 0.0f); + applyParamValue ("delayDryWet", pr.delayWet); + applyParamValue ("reverbRoom", pr.reverbRoom); + applyParamValue ("reverbStrength", pr.reverbLevel); + applyParamValue ("reverbDiffusion", pr.reverbDiff); + applyParamValue ("reverbDryWet", pr.reverbWet); } void MonostepAudioProcessor::randomizePattern() @@ -420,11 +445,66 @@ void MonostepAudioProcessor::randomizeFx() applyParamValue ("drive", random.nextFloat()); applyParamValue ("ringmod", random.nextFloat()); applyParamValue ("delayTime", random.nextFloat()); - applyParamValue ("delayStrength", random.nextFloat() * 0.8f); + applyParamValue ("delayStrength", random.nextFloat() * 0.9f); applyParamValue ("delaySync", random.nextFloat() < 0.8f ? 1.0f : 0.0f); + applyParamValue ("delayDryWet", random.nextFloat() * 0.8f); applyParamValue ("reverbRoom", 0.2f + random.nextFloat() * 0.7f); - applyParamValue ("reverbStrength", random.nextFloat() * 0.7f); + applyParamValue ("reverbStrength", random.nextFloat()); applyParamValue ("reverbDiffusion", random.nextFloat()); + applyParamValue ("reverbDryWet", random.nextFloat() * 0.8f); +} + +void MonostepAudioProcessor::randomizeAccent() +{ + const int length = juce::roundToInt (seqLenParam->load()); + + for (int i = 0; i < length; ++i) + { + auto& s = sequencer.step (i); + if (! s.gate) + continue; + s.accent = random.nextFloat() < 0.3f; + } + + if (onPatternChanged) + onPatternChanged(); +} + +void MonostepAudioProcessor::randomizeSlide() +{ + const int length = juce::roundToInt (seqLenParam->load()); + + for (int i = 0; i < length; ++i) + { + auto& s = sequencer.step (i); + if (! s.gate) + continue; + s.slide = random.nextFloat() < 0.25f; + } + + if (onPatternChanged) + onPatternChanged(); +} + +void MonostepAudioProcessor::randomizeFine() +{ + const int length = juce::roundToInt (seqLenParam->load()); + + for (int i = 0; i < length; ++i) + { + auto& s = sequencer.step (i); + if (! s.gate) + continue; + s.cents = random.nextFloat() * 100.0f - 50.0f; + } + + if (onPatternChanged) + onPatternChanged(); +} + +void MonostepAudioProcessor::randomizeRate() +{ + applyParamValue ("seqRate", (float) random.nextInt (5)); } void MonostepAudioProcessor::randomizeParams() @@ -686,8 +766,9 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer& buffer, juc } // Accent tracks the current step (not just gate changes), so it holds for - // the whole step even when the note repeats without retriggering. - voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f); + // the whole step even when the note repeats without retriggering. Kept + // gentle: a modest volume lift plus a bit of brightness/timing (below). + voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 0.4f : 1.0f); if (wantGate != lastGateApplied) { @@ -714,13 +795,15 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer& buffer, juc buffer.addFrom (ch, 0, scratch, 0, 0, numSamples); monostep::FxProcessor::Params fp; - fp.delayTime = delayTimeParam->load(); - fp.delayMix = delayStrengthParam->load(); - fp.delaySync = delaySyncParam->load() >= 0.5f; - fp.reverbRoom = reverbRoomParam->load(); - fp.reverbMix = reverbStrengthParam->load(); - fp.reverbDiff = reverbDiffusionParam->load(); - fp.bpm = bpm; + fp.delayTime = delayTimeParam->load(); + fp.delayFeedback = delayStrengthParam->load(); + fp.delaySync = delaySyncParam->load() >= 0.5f; + fp.delayMix = delayDryWetParam->load(); + fp.reverbRoom = reverbRoomParam->load(); + fp.reverbLevel = reverbStrengthParam->load(); + fp.reverbMix = reverbDryWetParam->load(); + fp.reverbDiff = reverbDiffusionParam->load(); + fp.bpm = bpm; fx.setParams (fp); fx.process (buffer, numSamples); } diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 7aeb056..fd675dc 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -59,6 +59,10 @@ public: void randomizeEnv(); void randomizeSeqSettings(); void randomizeFx(); + void randomizeAccent(); + void randomizeSlide(); + void randomizeFine(); + void randomizeRate(); juce::String getStepNoteName (int idx) const; @@ -110,9 +114,11 @@ private: std::atomic* delayTimeParam = nullptr; std::atomic* delayStrengthParam = nullptr; std::atomic* delaySyncParam = nullptr; + std::atomic* delayDryWetParam = nullptr; std::atomic* reverbRoomParam = nullptr; std::atomic* reverbStrengthParam = nullptr; std::atomic* reverbDiffusionParam = nullptr; + std::atomic* reverbDryWetParam = nullptr; monostep::StepSequencer sequencer; monostep::SynthVoice voice; diff --git a/Source/dsp/FxProcessor.cpp b/Source/dsp/FxProcessor.cpp index 4d8e288..5e2b4bf 100644 --- a/Source/dsp/FxProcessor.cpp +++ b/Source/dsp/FxProcessor.cpp @@ -7,17 +7,19 @@ namespace monostep { // Freeverb-style comb / allpass sizes, slightly offset per channel so the -// left and right reverb tails decorrelate. +// left and right reverb tails decorrelate. Halved from the classic freeverb +// set so the early reflections land ~13 ms after the note instead of ~25 ms: +// a 100%-wet reverb keeps its tail but speaks much sooner. static constexpr int combSizes[2][4] = { - { 1116, 1188, 1277, 1356 }, - { 1126, 1198, 1287, 1366 } + { 558, 594, 639, 678 }, + { 563, 599, 644, 683 } }; static constexpr int allpassSizes[2][2] = { - { 225, 556 }, - { 235, 566 } + { 113, 278 }, + { 118, 283 } }; float FxProcessor::delayTimeSeconds (float knob, bool sync, double bpm) @@ -36,7 +38,7 @@ juce::String FxProcessor::delayTimeLabel (float knob, bool sync, double bpm) { if (sync) { - static const char* names[] = { "1/16", "1/8", "1/8D", "1/4", "1/4D", "1/2", "1/2D", "1W" }; + static const char* names[] = { "1/16", "1/8", "3/16", "1/4", "3/8", "1/2", "3/4", "1 bar" }; const int idx = juce::roundToInt (juce::jlimit (0.0f, 1.0f, knob) * 7.0f); return names[juce::jlimit (0, 7, idx)]; } @@ -112,13 +114,15 @@ void FxProcessor::reset() void FxProcessor::setParams (const Params& p) { - params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime); - params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix); - params.delaySync = p.delaySync; - params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom); - params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix); - params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff); - params.bpm = juce::jmax (20.0, p.bpm); + params.delayTime = juce::jlimit (0.0f, 1.0f, p.delayTime); + params.delayFeedback = juce::jlimit (0.0f, 0.9f, p.delayFeedback); + params.delaySync = p.delaySync; + params.delayMix = juce::jlimit (0.0f, 1.0f, p.delayMix); + params.reverbRoom = juce::jlimit (0.0f, 1.0f, p.reverbRoom); + params.reverbLevel = juce::jlimit (0.0f, 1.0f, p.reverbLevel); + params.reverbMix = juce::jlimit (0.0f, 1.0f, p.reverbMix); + params.reverbDiff = juce::jlimit (0.0f, 1.0f, p.reverbDiff); + params.bpm = juce::jmax (20.0, p.bpm); currentDelaySeconds = delayTimeSeconds (params.delayTime, params.delaySync, params.bpm); } @@ -128,8 +132,23 @@ void FxProcessor::process (juce::AudioBuffer& buffer, int numSamples) if (numSamples <= 0 || buffer.getNumChannels() == 0) return; + // The reverb must feed off the dry (pre-delay) signal, not the delay's + // output, so snapshot the incoming buffer before the delay runs. + const bool reverbActive = params.reverbMix > 0.0001f && params.reverbLevel > 0.0001f; + if (reverbActive) + { + if (dryScratch.getNumSamples() < numSamples) + dryScratch.setSize (buffer.getNumChannels(), numSamples, false, false, true); + + const int n = juce::jmin (buffer.getNumChannels(), dryScratch.getNumChannels()); + for (int c = 0; c < n; ++c) + dryScratch.copyFrom (c, 0, buffer, c, 0, numSamples); + } + processDelay (buffer, numSamples); - processReverb (buffer, numSamples); + + if (reverbActive) + processReverb (dryScratch, buffer, numSamples); } void FxProcessor::processDelay (juce::AudioBuffer& buffer, int numSamples) @@ -166,7 +185,7 @@ void FxProcessor::processDelay (juce::AudioBuffer& buffer, int numSamples // Feedback with a little high-frequency damping. d.feedbackLp += 0.35f * (delayed - d.feedbackLp); - d.memory[d.writeIndex] = x + 0.42f * d.feedbackLp; + d.memory[d.writeIndex] = x + params.delayFeedback * d.feedbackLp; out[i] = x + delayed * mix; @@ -176,24 +195,38 @@ void FxProcessor::processDelay (juce::AudioBuffer& buffer, int numSamples } } -void FxProcessor::processReverb (juce::AudioBuffer& buffer, int numSamples) +void FxProcessor::processReverb (const juce::AudioBuffer& dryIn, + juce::AudioBuffer& buffer, int numSamples) { - if (params.reverbMix <= 0.0001f) + if (params.reverbMix <= 0.0001f || params.reverbLevel <= 0.0001f) return; - const int n = juce::jmin (numChannels, buffer.getNumChannels()); + const int n = juce::jmin (numChannels, juce::jmin (dryIn.getNumChannels(), buffer.getNumChannels())); const float combGain = 0.70f + 0.28f * params.reverbRoom; // 0.70 .. 0.98 const float allpassGain = 0.75f * params.reverbDiff; // 0 .. 0.75 + // Room normalisation: the comb network's DC gain is ~4 / (1 - combGain), + // so scaling by (1 - combGain) keeps the wet level bounded as the room + // (decay length) grows instead of blowing up near oscillation. The 2.5 + // constant is calibrated (standalone replication of this network): at + // room 0.5 it puts the note-off ring ~ +6 dB above the dry and the tail + // ~ -6 dB one second in, so the reverb is clearly audible even at modest + // dry/wet settings. The wet is intentionally NOT clipped here: any + // soft-clip knee far below the signal level pins the wet at a constant + // flat value (the early tail stops audibly decaying, which reads as a + // "reverb slowly kicking in" plateau) and makes 100% wet sound quiet + // next to a hot dry signal. + const float wetScale = 2.5f * (1.0f - combGain); const float mix = params.reverbMix; for (int ch = 0; ch < n; ++ch) { auto& r = reverbs[ch]; + const float* in = dryIn.getReadPointer (ch); auto* out = buffer.getWritePointer (ch); for (int i = 0; i < numSamples; ++i) { - const float x = out[i]; + const float x = in[i]; float combSum = 0.0f; for (int c = 0; c < 4; ++c) @@ -208,7 +241,7 @@ void FxProcessor::processReverb (juce::AudioBuffer& buffer, int numSample pos = 0; } - float wet = combSum * 0.012f; + float wet = combSum * wetScale * params.reverbLevel; for (int a = 0; a < 2; ++a) { @@ -223,7 +256,7 @@ void FxProcessor::processReverb (juce::AudioBuffer& buffer, int numSample pos = 0; } - out[i] = x * (1.0f - mix) + wet * mix; + out[i] += wet * mix; } } } diff --git a/Source/dsp/FxProcessor.h b/Source/dsp/FxProcessor.h index e3905bd..4fb3f4e 100644 --- a/Source/dsp/FxProcessor.h +++ b/Source/dsp/FxProcessor.h @@ -17,13 +17,15 @@ public: struct Params { - float delayTime = 0.35f; // normalized knob 0..1 - float delayMix = 0.0f; // 0..1 dry/wet - bool delaySync = true; // tempo-synced vs. free milliseconds - float reverbRoom = 0.5f; // 0..1 - float reverbMix = 0.0f; // 0..1 dry/wet - float reverbDiff = 0.6f; // 0..1 allpass diffusion - double bpm = 120.0; + float delayTime = 0.35f; // normalized knob 0..1 + float delayFeedback = 0.45f; // 0..0.9 feedback (repeat density) + bool delaySync = true; // tempo-synced vs. free milliseconds + float delayMix = 0.0f; // 0..1 dry/wet + float reverbRoom = 0.5f; // 0..1 + float reverbLevel = 1.0f; // 0..1 wet send gain + float reverbMix = 0.0f; // 0..1 dry/wet + float reverbDiff = 0.6f; // 0..1 allpass diffusion + double bpm = 120.0; }; void setParams (const Params& p); @@ -52,7 +54,7 @@ private: }; void processDelay (juce::AudioBuffer& buffer, int numSamples); - void processReverb (juce::AudioBuffer& buffer, int numSamples); + void processReverb (const juce::AudioBuffer& dryIn, juce::AudioBuffer& buffer, int numSamples); double sampleRate = 44100.0; int numChannels = 2; @@ -64,6 +66,10 @@ private: Params params; float currentDelaySeconds = 0.0f; + // Pre-delay snapshot: the reverb is fed the dry signal (before the delay), + // so delay and reverb run in parallel instead of in series. + juce::AudioBuffer dryScratch; + std::vector delays; std::vector reverbs; }; diff --git a/Source/dsp/SynthVoice.h b/Source/dsp/SynthVoice.h index 4ef684b..fe2d091 100644 --- a/Source/dsp/SynthVoice.h +++ b/Source/dsp/SynthVoice.h @@ -137,6 +137,10 @@ private: fenvStage = Stage::attack; } + // RC-style (exponential) filter envelope: the cutoff glides smoothly instead + // of stepping linearly, so fast filter sweeps sound analog rather than + // zippered. Each stage still reaches ~95% within the labelled time, so the + // knob values keep their musical meaning. void updateFilterEnvelope() { const float sr = sampleRate; @@ -144,23 +148,37 @@ private: switch (fenvStage) { case Stage::attack: - fenv += 1.0f / (params.fAttack * sr); - if (fenv >= 1.0f) { fenv = 1.0f; fenvStage = Stage::decay; } + { + // Accented notes attack the filter ever so slightly faster. + const float acc = accentSmooth - 1.0f; + const float t = juce::jmax (0.0005f, params.fAttack / (1.0f + acc * 0.5f)); + const float a = 1.0f - std::exp (-3.0f / (t * sr)); + fenv += (1.0f - fenv) * a; + if (fenv >= 0.999f) { fenv = 1.0f; fenvStage = Stage::decay; } break; + } case Stage::decay: - fenv -= (1.0f - params.fSustain) / (params.fDecay * sr); - if (fenv <= params.fSustain) { fenv = params.fSustain; fenvStage = Stage::sustain; } + { + const float t = juce::jmax (0.0005f, params.fDecay); + const float a = 1.0f - std::exp (-3.0f / (t * sr)); + fenv += (params.fSustain - fenv) * a; + if (std::fabs (fenv - params.fSustain) < 0.001f) { fenv = params.fSustain; fenvStage = Stage::sustain; } break; + } case Stage::sustain: fenv = params.fSustain; break; case Stage::release: - fenv -= 1.0f / (params.fRelease * sr); - if (fenv <= 0.0f) { fenv = 0.0f; fenvStage = Stage::idle; } + { + const float t = juce::jmax (0.0005f, params.fRelease); + const float a = 1.0f - std::exp (-3.0f / (t * sr)); + fenv -= fenv * a; + if (fenv <= 0.001f) { fenv = 0.0f; fenvStage = Stage::idle; } break; + } case Stage::idle: break; @@ -278,15 +296,18 @@ private: float renderSample() { - updateEnvelope(); - updateFilterEnvelope(); - updateGlide(); - // Accent gains fast attack / slow release, so the boost fades out smoothly // at the tail of an accented note instead of cutting off hard-edged. accentSmooth += (accentLevel - accentSmooth) * (accentLevel > accentSmooth ? accentAttackCoef : accentReleaseCoef); + // 0 for unaccented steps, rising with the accent knob on accented ones. + const float accBoost = accentSmooth - 1.0f; + + updateEnvelope(); + updateFilterEnvelope(); + updateGlide(); + const float incA = (currentFreq * params.detuneRatioA) / sampleRate; const float incB = (currentFreq * params.detuneRatio) / sampleRate; @@ -302,14 +323,17 @@ private: out = (1.0f - params.ringMod) * out + params.ringMod * (a * b); const float preFilter = out; + + // Accented steps open the filter a tiny bit, so they sparkle without + // relying on a big volume jump. const float envCutoff = juce::jlimit (20.0f, 20000.0f, - params.cutoff * std::pow (2.0f, params.fAmount * fenv)); + params.cutoff * std::pow (2.0f, params.fAmount * fenv + accBoost * 1.5f)); out = processFilter (out, envCutoff); // Accented steps also get a touch of the unfiltered signal so they cut // through the mix (presence), not just a volume bump. - if (accentSmooth > 1.0f) - out += (accentSmooth - 1.0f) * 0.2f * preFilter; + if (accBoost > 0.0f) + out += accBoost * 0.2f * preFilter; if (params.drive > 0.0f) { diff --git a/cmake/WriteBuildStamp.cmake b/cmake/WriteBuildStamp.cmake new file mode 100644 index 0000000..d51cecd --- /dev/null +++ b/cmake/WriteBuildStamp.cmake @@ -0,0 +1,22 @@ +# Generates BuildStamp.h with the current date/time. Run as a build-time +# custom command so the stamp is refreshed on every build (the TU that includes +# it is recompiled because the header mtime changes each time). + +if(NOT DEFINED MONOSTEP_BUILD_STAMP_HEADER) + message(FATAL_ERROR "MONOSTEP_BUILD_STAMP_HEADER must be set") +endif() + +string(TIMESTAMP STAMP "%Y-%m-%d__%H:%M:%S") + +get_filename_component(MONOSTEP_BUILD_STAMP_DIR "${MONOSTEP_BUILD_STAMP_HEADER}" DIRECTORY) +file(MAKE_DIRECTORY "${MONOSTEP_BUILD_STAMP_DIR}") + +file(WRITE "${MONOSTEP_BUILD_STAMP_HEADER}" +"// Generated at build time - do not edit. +#ifndef MONOSTEP_BUILD_STAMP_H_INCLUDED +#define MONOSTEP_BUILD_STAMP_H_INCLUDED + +#define MONOSTEP_BUILD_DATE \"${STAMP}\" + +#endif +") diff --git a/tests/TestHost.cpp b/tests/TestHost.cpp index bdf8bb1..735aa36 100644 --- a/tests/TestHost.cpp +++ b/tests/TestHost.cpp @@ -305,6 +305,8 @@ static int testProcessorRendering() *processor.getAPVTS().getRawParameterValue ("release") = 0.01f; *processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.0f; *processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.0f; + *processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.0f; + *processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.0f; const int releaseBlock = numBlocks / 2; @@ -360,9 +362,11 @@ static int testProcessorRendering() *processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f; *processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f; *processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f; + *processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.5f; *processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f; *processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f; *processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f; + *processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.5f; const double wetTail = renderFxTail();