MonoStep: monophonic 2-oscillator step-sequencer synth

This commit is contained in:
armin 2026-08-05 19:55:57 +02:00
commit e52e5cb161
14 changed files with 2674 additions and 0 deletions

38
.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
# Build output
/build/
/_artefacts/
/*_artefacts/
*.vst3
*.vst3.bundle
*.component
*.aaxplugin
*.app
*.lv2
# CMake
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
CMakeOutput.log
CMakeError.log
cmake-build-*/
Makefile
*.ninja
# Xcode
*.xcodeproj
*.xcworkspace
xcuserdata/
DerivedData/
# IDE / editors
.vscode/
.idea/
.vs/
*.sublime-project
*.sublime-workspace
*.swp
*~
# macOS
.DS_Store

97
CMakeLists.txt Normal file
View file

@ -0,0 +1,97 @@
cmake_minimum_required(VERSION 3.22)
project(MonoStep VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
set(CMAKE_OSX_ARCHITECTURES "arm64")
# --- build metadata (git info + build date) ----------------------------------
set(MONOSTEP_GIT_STRING "no-vcs")
find_package(Git QUIET)
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} -C "${CMAKE_CURRENT_LIST_DIR}" rev-parse --short HEAD
OUTPUT_VARIABLE MONOSTEP_GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
execute_process(
COMMAND ${GIT_EXECUTABLE} -C "${CMAKE_CURRENT_LIST_DIR}" status --porcelain
OUTPUT_VARIABLE MONOSTEP_GIT_PORCELAIN
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(MONOSTEP_GIT_HASH)
set(MONOSTEP_GIT_STRING "${MONOSTEP_GIT_HASH}")
if(NOT MONOSTEP_GIT_PORCELAIN STREQUAL "")
string(APPEND MONOSTEP_GIT_STRING "-dirty")
endif()
endif()
endif()
if(NOT DEFINED JUCE_DIR)
set(JUCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../juce")
endif()
if(NOT EXISTS "${JUCE_DIR}/CMakeLists.txt")
message(FATAL_ERROR "JUCE not found at ${JUCE_DIR}. Pass -DJUCE_DIR=<path> or clone it next to this project.")
endif()
add_subdirectory("${JUCE_DIR}" juce_build)
juce_add_plugin(MonoStep
VERSION 1.0.0
COMPANY_NAME "armin"
BUNDLE_ID "com.armin.monostep"
FORMATS VST3
PRODUCT_NAME "MonoStep"
PLUGIN_MANUFACTURER_CODE Armn
PLUGIN_CODE MStp
DESCRIPTION "Monophonic 2-oscillator step-sequencer synth"
EDITOR_NAME MonoStepAudioProcessorEditor
IS_SYNTH TRUE
NEEDS_MIDI_INPUT TRUE
VST3_AUTO_MANIFEST TRUE
)
juce_generate_juce_header(MonoStep)
target_sources(MonoStep PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/ui/SequencerMatrix.cpp
)
target_include_directories(MonoStep PRIVATE Source)
target_compile_definitions(MonoStep PRIVATE
JUCE_WEB_BROWSER=0
JUCE_VST3_CAN_REPLACE_VST2=0
MONOSTEP_GIT_INFO="${MONOSTEP_GIT_STRING}"
)
option(MONOSTEP_BUILD_TESTS "Build the headless host test" ON)
if(MONOSTEP_BUILD_TESTS)
juce_add_console_app(MonoStepTestHost
PRODUCT_NAME MonoStepTestHost
)
juce_generate_juce_header(MonoStepTestHost)
target_link_libraries(MonoStepTestHost PRIVATE
juce::juce_audio_processors
juce::juce_audio_formats
)
target_include_directories(MonoStepTestHost PRIVATE Source)
target_sources(MonoStepTestHost PRIVATE
Source/PluginProcessor.cpp
tests/TestHost.cpp
)
target_compile_definitions(MonoStepTestHost PRIVATE
MONOSTEP_HEADLESS=1
JUCE_PLUGINHOST_VST3=1
)
endif()

48
Source/BuildInfo.h Normal file
View file

@ -0,0 +1,48 @@
#pragma once
#include <JuceHeader.h>
namespace monostep
{
namespace buildinfo
{
inline juce::String gitInfo()
{
#if defined (MONOSTEP_GIT_INFO)
return juce::String (MONOSTEP_GIT_INFO);
#else
return "no-vcs";
#endif
}
inline juce::String buildDateString()
{
static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
const juce::String d (__DATE__);
int month = 1;
for (int i = 0; i < 12; ++i)
if (d.startsWith (months[i]))
{
month = i + 1;
break;
}
const int day = d.substring (4, 6).trim().getIntValue();
const int year = d.substring (7).getIntValue();
return juce::String::formatted ("%04d-%02d-%02d__%s", year, month, day, __TIME__);
}
inline juce::String footerText()
{
return gitInfo() + " (" + buildDateString() + ")";
}
} // namespace buildinfo
} // namespace monostep

613
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,613 @@
#include "PluginEditor.h"
#include "PluginProcessor.h"
#include "Theme.h"
#include "BuildInfo.h"
using namespace monostep;
//==============================================================================
EditorLookAndFeel::EditorLookAndFeel()
{
setColour (juce::Slider::textBoxBackgroundColourId, colours::panel);
setColour (juce::Slider::textBoxTextColourId, colours::text);
setColour (juce::Slider::textBoxOutlineColourId, colours::grid);
}
void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h,
float sliderPos, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider&)
{
const auto bounds = juce::Rectangle<float> ((float) x, (float) y, (float) w, (float) h);
const auto centre = bounds.getCentre();
const float radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
const float start = rotaryStartAngle;
const float end = rotaryEndAngle;
const float angle = start + sliderPos * (end - start);
g.setColour (colours::grid);
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
g.setColour (colours::gridLight);
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
if (end - start < 6.0f)
{
juce::Path valueArc;
valueArc.addCentredArc (centre.getX(), centre.getY(),
radius * 0.85f, radius * 0.85f,
0.0f, start, angle, true);
g.setColour (colours::accent);
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
}
// JUCE measures rotary angles clockwise from 12 o'clock; the pointer is
// drawn with the same convention so it matches the arc and mouse dragging.
const float tickLen = radius * 0.62f;
g.setColour (colours::accent);
g.drawLine (centre.getX(), centre.getY(),
centre.getX() + tickLen * std::sin (angle),
centre.getY() - tickLen * std::cos (angle),
2.5f);
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f);
}
void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool, bool)
{
const auto bounds = button.getLocalBounds().toFloat();
g.setColour (button.getToggleState() ? colours::accent : colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
g.setFont (font (11.0f));
g.setColour (button.getToggleState() ? colours::bg : colours::text);
g.drawText (button.getButtonText(), bounds, juce::Justification::centred);
}
void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour&, bool highlighted, bool)
{
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState())
g.setColour (colours::accent);
else
g.setColour (highlighted ? colours::gridLight : colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
}
void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool)
{
g.setFont (font (10.5f));
g.setColour (button.getToggleState() ? colours::bg : colours::text);
g.drawText (button.getButtonText(), button.getLocalBounds(), juce::Justification::centred);
}
//==============================================================================
Knob::Knob (const juce::String& title)
{
slider.setSliderStyle (juce::Slider::RotaryVerticalDrag);
slider.setRotaryParameters (juce::MathConstants<float>::pi * 1.25f,
juce::MathConstants<float>::pi * 2.75f,
true);
slider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
slider.onValueChange = [this]
{
updateValueLabel();
if (customOnValueChange != nullptr)
customOnValueChange ((float) slider.getValue());
};
addAndMakeVisible (slider);
titleLabel.setText (title, juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centred);
titleLabel.setFont (font (10.0f));
titleLabel.setColour (juce::Label::textColourId, colours::text);
addAndMakeVisible (titleLabel);
valueLabel.setJustificationType (juce::Justification::centred);
valueLabel.setFont (font (9.0f));
valueLabel.setColour (juce::Label::textColourId, colours::accent);
addAndMakeVisible (valueLabel);
updateValueLabel();
}
void Knob::resized()
{
const int w = getWidth();
titleLabel.setBounds (0, 0, w, 14);
slider.setBounds (4, 15, w - 8, w - 8);
valueLabel.setBounds (0, w - 8 + 15, w, 12);
}
void Knob::updateValueLabel()
{
const float value = slider.getValue();
if (format)
valueLabel.setText (format (value), juce::dontSendNotification);
else
valueLabel.setText (juce::String (value, 2), juce::dontSendNotification);
}
//==============================================================================
ChoiceBar::ChoiceBar (juce::AudioProcessorValueTreeState& apvtsRef,
const juce::String& id,
const juce::StringArray& choices)
: apvts (&apvtsRef), paramId (id)
{
for (int i = 0; i < choices.size(); ++i)
{
auto* button = buttons.add (new juce::TextButton (choices[i]));
button->setClickingTogglesState (true);
button->setRadioGroupId (1);
button->onClick = [this, i] { setIndex (i); };
addAndMakeVisible (button);
}
apvts->addParameterListener (paramId, this);
refreshFromParam();
}
ChoiceBar::ChoiceBar (const juce::StringArray& choices,
std::function<void (int)> onSelected)
: manualHandler (std::move (onSelected))
{
for (int i = 0; i < choices.size(); ++i)
{
auto* button = buttons.add (new juce::TextButton (choices[i]));
button->setClickingTogglesState (true);
button->setRadioGroupId (1);
button->onClick = [this, i] { setIndex (i); };
addAndMakeVisible (button);
}
buttons.getFirst()->setToggleState (true, juce::dontSendNotification);
}
ChoiceBar::~ChoiceBar()
{
if (apvts != nullptr)
apvts->removeParameterListener (paramId, this);
}
void ChoiceBar::parameterChanged (const juce::String&, float)
{
juce::MessageManager::callAsync ([this] { refreshFromParam(); });
}
void ChoiceBar::refreshFromParam()
{
if (apvts == nullptr)
return;
if (auto* parameter = apvts->getParameter (paramId))
{
const int index = juce::roundToInt (parameter->convertFrom0to1 (parameter->getValue()));
for (int i = 0; i < buttons.size(); ++i)
buttons[i]->setToggleState (i == index, juce::dontSendNotification);
}
}
void ChoiceBar::setIndex (int index)
{
if (apvts != nullptr)
{
if (auto* parameter = apvts->getParameter (paramId))
{
parameter->beginChangeGesture();
parameter->setValueNotifyingHost (parameter->convertTo0to1 ((float) index));
parameter->endChangeGesture();
}
return;
}
if (manualHandler != nullptr)
manualHandler (index);
}
void ChoiceBar::resized()
{
const int count = buttons.size();
const float spacing = 3.0f;
const float w = ((float) getWidth() - spacing * (count - 1)) / (float) count;
for (int i = 0; i < count; ++i)
buttons[i]->setBounds (juce::roundToInt (i * (w + spacing)), 0,
juce::roundToInt (w), getHeight());
}
//==============================================================================
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
: processor (processorRef),
fineKnob ("Fine")
{
titleLabel.setText ("Step", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centred);
titleLabel.setFont (font (12.0f));
titleLabel.setColour (juce::Label::textColourId, colours::text);
addAndMakeVisible (titleLabel);
noteLabel.setJustificationType (juce::Justification::centred);
noteLabel.setFont (font (20.0f));
noteLabel.setColour (juce::Label::textColourId, colours::accent);
addAndMakeVisible (noteLabel);
gateLabel.setJustificationType (juce::Justification::centred);
gateLabel.setFont (font (10.0f));
gateLabel.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (gateLabel);
auto& fineSlider = fineKnob.getSlider();
fineSlider.setRange (-50.0, 50.0, 1.0);
fineSlider.setValue (0.0, juce::dontSendNotification);
fineKnob.setFormat ([] (float v) { return juce::String (v, 0) + " ct"; });
fineKnob.setOnValueChanged ([this] (float v)
{
if (! updating)
processor.setStepCents (selectedStep, v);
});
addAndMakeVisible (fineKnob);
prevButton.setButtonText ("<");
prevButton.onClick = [this] { setSelectedStep (selectedStep - 1); };
addAndMakeVisible (prevButton);
nextButton.setButtonText (">");
nextButton.onClick = [this] { setSelectedStep (selectedStep + 1); };
addAndMakeVisible (nextButton);
hintLabel.setText ("click a column in the grid or use the arrows to pick the step to fine-tune", juce::dontSendNotification);
hintLabel.setJustificationType (juce::Justification::centred);
hintLabel.setFont (font (8.5f));
hintLabel.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (hintLabel);
clearButton.setButtonText ("Clear Pattern");
clearButton.onClick = [this] { clearPattern(); };
addAndMakeVisible (clearButton);
setSelectedStep (0);
}
void StepPanel::setSelectedStep (int stepIdx)
{
if (stepIdx < 0 || stepIdx >= numSteps)
return;
if (stepIdx != selectedStep)
{
selectedStep = stepIdx;
if (onStepSelected)
onStepSelected (stepIdx);
}
refresh();
}
void StepPanel::refresh()
{
const auto& s = processor.getSequencer().step (selectedStep);
updating = true;
titleLabel.setText ("Step " + juce::String (selectedStep + 1), juce::dontSendNotification);
noteLabel.setText (processor.getStepNoteName (selectedStep)
+ (std::fabs (s.cents) > 0.5f ? " (" + formatCents (s.cents) + ")" : ""),
juce::dontSendNotification);
gateLabel.setText (s.gate ? "gate: on" : "gate: off", juce::dontSendNotification);
fineKnob.getSlider().setValue (s.cents, juce::dontSendNotification);
updating = false;
}
void StepPanel::clearPattern()
{
for (int i = 0; i < numSteps; ++i)
{
processor.setStepGate (i, false);
processor.setStepNote (i, 0);
processor.setStepCents (i, 0.0f);
processor.setStepSlide (i, false);
}
refresh();
}
void StepPanel::resized()
{
const int w = getWidth();
titleLabel.setBounds (0, 4, w, 16);
noteLabel.setBounds (0, 22, w, 28);
gateLabel.setBounds (0, 50, w, 14);
const int knobX = (w - 74) / 2;
const int knobY = 68;
fineKnob.setBounds (knobX, knobY, 74, 74);
const int btnY = knobY + 25;
prevButton.setBounds (knobX - 32, btnY, 24, 24);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
hintLabel.setBounds (8, knobY + 82, w - 16, 14);
clearButton.setBounds (10, knobY + 106, w - 20, 26);
}
//==============================================================================
MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor)
: AudioProcessorEditor (processor),
processorRef (processor),
content (*this),
lnf (std::make_unique<EditorLookAndFeel>()),
lengthKnob ("Length"),
swingKnob ("Swing"),
rootKnob ("Root"),
gateKnob ("Gate"),
masterKnob ("Master"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
cutoffKnob ("Cutoff"),
resKnob ("Res"),
glideKnob ("Glide"),
attackKnob ("Attack"),
decayKnob ("Decay"),
sustainKnob ("Sustain"),
releaseKnob ("Release"),
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}),
wave1Bar (processor.getAPVTS(), "osc1Wave", { "Sin", "Tri", "Saw", "Sq" }),
wave2Bar (processor.getAPVTS(), "osc2Wave", { "Sin", "Tri", "Saw", "Sq" }),
matrix (processor),
stepPanel (processor)
{
setLookAndFeel (lnf.get());
setSize (baseWidth, baseHeight);
auto& apvts = processor.getAPVTS();
addAndMakeVisible (content);
content.addAndMakeVisible (rateBar);
content.addAndMakeVisible (octaveBar);
content.addAndMakeVisible (zoomBar);
content.addAndMakeVisible (wave1Bar);
content.addAndMakeVisible (wave2Bar);
static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
zoomBar.setManualHandler ([this] (int idx)
{
setZoomFactor (zoomValues[juce::jlimit (0, 4, idx)]);
});
auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt)
{
content.addAndMakeVisible (knob);
knob.attach (apvts, id);
knob.setFormat (std::move (fmt));
};
addKnob (lengthKnob, "seqLen", [] (float v) { return juce::String ((int) v); });
addKnob (swingKnob, "seqSwing", [] (float v) { return juce::String (v, 2); });
addKnob (rootKnob, "seqRoot", [] (float v) { return juce::String ((int) v); });
addKnob (gateKnob, "seqGateLen", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (masterKnob, "master", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (detuneKnob, "detune", [] (float v) { return juce::String (juce::roundToInt (v)) + " ct"; });
addKnob (mixKnob, "mix", [] (float v) { return juce::String (v, 2); });
addKnob (cutoffKnob, "cutoff", [] (float v) { return v >= 1000.0f ? juce::String (juce::roundToInt (v / 100.0f) * 100) + " Hz" : juce::String (juce::roundToInt (v)) + " Hz"; });
addKnob (resKnob, "res", [] (float v) { return juce::String (v, 2); });
addKnob (glideKnob, "glide", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (attackKnob, "attack", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
content.addAndMakeVisible (matrix);
content.addAndMakeVisible (stepPanel);
matrix.onStepSelected = [this] (int stepIdx)
{
stepPanel.setSelectedStep (stepIdx);
};
stepPanel.onStepSelected = [this] (int stepIdx)
{
matrix.setSelectedStep (stepIdx);
};
lengthKnob.setOnValueChanged ([this] (float)
{
matrix.repaint();
});
matrix.onGateChanged = [this] (int stepIdx, bool gate)
{
processorRef.setStepGate (stepIdx, gate);
stepPanel.refresh();
};
matrix.onNoteChanged = [this] (int stepIdx, int semitone)
{
processorRef.setStepNote (stepIdx, semitone);
stepPanel.refresh();
};
matrix.onFineChanged = [this] (int stepIdx, float cents)
{
processorRef.setStepCents (stepIdx, cents);
stepPanel.refresh();
};
matrix.onSlideChanged = [this] (int stepIdx, bool slide)
{
processorRef.setStepSlide (stepIdx, slide);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
refreshEditor();
}
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
void MonoStepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
if (step != lastPlayStep)
{
lastPlayStep = step;
matrix.setPlayPosition (step);
}
}
void MonoStepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
{
g.setFont (font (22.0f));
g.setColour (colours::text);
g.drawText ("MonoStep", 16, 12, 130, 24, juce::Justification::centredLeft);
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("2 OSC - step sequencer", 16, 34, 130, 12, juce::Justification::centredLeft);
const auto drawCaption = [&] (const juce::Component& component, const juce::String& text)
{
g.drawText (text,
component.getX(), component.getY() - 13,
component.getWidth(), 12,
juce::Justification::centred);
};
drawCaption (rateBar, "RATE");
drawCaption (octaveBar, "OCTAVE");
drawCaption (zoomBar, "ZOOM");
drawCaption (wave1Bar, "OSC 1");
drawCaption (wave2Bar, "OSC 2");
const int footY = baseHeight - 20;
g.setColour (colours::grid);
g.fillRect (0, footY, baseWidth, 20);
g.setColour (colours::panel);
g.drawHorizontalLine (footY, 0, (float) baseWidth);
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
}
void MonoStepAudioProcessorEditor::setZoomFactor (float newZoom)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
void MonoStepAudioProcessorEditor::resized()
{
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
if (getWidth() != targetW || getHeight() != targetH)
{
setSize (targetW, targetH);
return;
}
content.setBounds (0, 0, baseWidth, baseHeight);
content.setTransform (juce::AffineTransform::scale (zoomFactor));
// All layout below happens in design units; the content transform scales it.
const int w = baseWidth;
const int h = baseHeight;
const int topBarH = 58;
const int bottomBarH = 96;
const int pad = 12;
lengthKnob.setBounds (128, 2, 54, 56);
rateBar.setBounds (192, 18, 210, 26);
swingKnob.setBounds (422, 2, 54, 56);
rootKnob.setBounds (482, 2, 54, 56);
octaveBar.setBounds (548, 18, 170, 26);
zoomBar.setBounds (w - pad - 54 - 8 - 236, 18, 236, 26);
masterKnob.setBounds (w - pad - 54, 2, 54, 56);
const int panelW = 200;
const int midTop = topBarH;
const int midH = h - topBarH - bottomBarH - pad;
stepPanel.setBounds (w - pad - panelW, midTop, panelW, midH);
matrix.setBounds (pad, midTop, w - pad * 2 - panelW - pad, midH);
const int bottomY = h - bottomBarH - pad;
int x = 16;
const int barW = 112;
wave1Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 8;
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56;
const int knobGap = 72;
const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob)
{
knob.setBounds (x, knobY, knobW, 82);
x += knobGap;
};
placeKnob (detuneKnob);
placeKnob (mixKnob);
placeKnob (cutoffKnob);
placeKnob (resKnob);
placeKnob (glideKnob);
placeKnob (attackKnob);
placeKnob (decayKnob);
placeKnob (sustainKnob);
placeKnob (releaseKnob);
}

190
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,190 @@
#pragma once
#include <JuceHeader.h>
#include "ui/SequencerMatrix.h"
class MonoStepAudioProcessor;
class EditorLookAndFeel final : public juce::LookAndFeel_V4
{
public:
EditorLookAndFeel();
void drawRotarySlider (juce::Graphics&, int x, int y, int w, int h,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider&) override;
void drawToggleButton (juce::Graphics&, juce::ToggleButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override;
void drawButtonBackground (juce::Graphics&, juce::Button&,
const juce::Colour& backgroundColour, bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;
void drawButtonText (juce::Graphics&, juce::TextButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override;
};
class Knob final : public juce::Component
{
public:
explicit Knob (const juce::String& title);
juce::Slider& getSlider() { return slider; }
void attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
{
attachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (apvts, paramId, slider);
}
void setFormat (std::function<juce::String (float)> formatFn)
{
format = std::move (formatFn);
updateValueLabel();
}
void setOnValueChanged (std::function<void (float)> valueChangedFn)
{
customOnValueChange = std::move (valueChangedFn);
}
void resized() override;
private:
void updateValueLabel();
juce::Slider slider;
juce::Label titleLabel;
juce::Label valueLabel;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
std::function<juce::String (float)> format;
std::function<void (float)> customOnValueChange;
};
class ChoiceBar final : public juce::Component,
public juce::AudioProcessorValueTreeState::Listener
{
public:
ChoiceBar (juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramId,
const juce::StringArray& choices);
ChoiceBar (const juce::StringArray& choices,
std::function<void (int)> onSelected);
~ChoiceBar() override;
void parameterChanged (const juce::String&, float) override;
void setManualHandler (std::function<void (int)> handler) { manualHandler = std::move (handler); }
void resized() override;
private:
void refreshFromParam();
void setIndex (int index);
juce::AudioProcessorValueTreeState* apvts = nullptr;
juce::String paramId;
std::function<void (int)> manualHandler;
juce::OwnedArray<juce::TextButton> buttons;
};
class StepPanel final : public juce::Component
{
public:
explicit StepPanel (MonoStepAudioProcessor& processor);
void setSelectedStep (int stepIdx);
void refresh();
std::function<void (int)> onStepSelected;
void resized() override;
private:
void clearPattern();
MonoStepAudioProcessor& processor;
int selectedStep = 0;
bool updating = false;
juce::Label titleLabel;
juce::Label noteLabel;
juce::Label gateLabel;
Knob fineKnob;
juce::TextButton prevButton;
juce::TextButton nextButton;
juce::Label hintLabel;
juce::TextButton clearButton;
};
class MonoStepAudioProcessorEditor final : public juce::AudioProcessorEditor,
public juce::Timer
{
public:
explicit MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor);
~MonoStepAudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
void timerCallback() override;
private:
void paintContent (juce::Graphics&);
class ContentComponent final : public juce::Component
{
public:
explicit ContentComponent (MonoStepAudioProcessorEditor& owner) : ownerRef (owner) {}
void paint (juce::Graphics& g) override { ownerRef.paintContent (g); }
private:
MonoStepAudioProcessorEditor& ownerRef;
};
MonoStepAudioProcessor& processorRef;
ContentComponent content;
static constexpr int baseWidth = 1060;
static constexpr int baseHeight = 640;
float zoomFactor = 1.0f;
void setZoomFactor (float newZoom);
std::unique_ptr<EditorLookAndFeel> lnf;
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> attachments;
Knob lengthKnob;
Knob swingKnob;
Knob rootKnob;
Knob gateKnob;
Knob masterKnob;
Knob detuneKnob;
Knob mixKnob;
Knob cutoffKnob;
Knob resKnob;
Knob glideKnob;
Knob attackKnob;
Knob decayKnob;
Knob sustainKnob;
Knob releaseKnob;
ChoiceBar rateBar;
ChoiceBar octaveBar;
ChoiceBar zoomBar;
ChoiceBar wave1Bar;
ChoiceBar wave2Bar;
SequencerMatrix matrix;
StepPanel stepPanel;
int lastPlayStep = -1;
void refreshEditor();
};

516
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,516 @@
#include "PluginProcessor.h"
#ifndef MONOSTEP_HEADLESS
#include "PluginEditor.h"
#endif
static const char* paramId (int index)
{
static const char* ids[] =
{
"osc1Wave", "osc2Wave", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master",
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen"
};
return ids[index];
}
enum ParamIndex
{
pOsc1Wave, pOsc2Wave, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
numParams
};
MonoStepAudioProcessor::MonoStepAudioProcessor()
: AudioProcessor (BusesProperties()
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
, apvts (*this, nullptr, "PARAMS", createParameterLayout())
{
osc1WaveParam = apvts.getRawParameterValue (paramId (pOsc1Wave));
osc2WaveParam = apvts.getRawParameterValue (paramId (pOsc2Wave));
detuneParam = apvts.getRawParameterValue (paramId (pDetune));
mixParam = apvts.getRawParameterValue (paramId (pMix));
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
resParam = apvts.getRawParameterValue (paramId (pRes));
attackParam = apvts.getRawParameterValue (paramId (pAttack));
decayParam = apvts.getRawParameterValue (paramId (pDecay));
sustainParam = apvts.getRawParameterValue (paramId (pSustain));
releaseParam = apvts.getRawParameterValue (paramId (pRelease));
glideParam = apvts.getRawParameterValue (paramId (pGlide));
masterParam = apvts.getRawParameterValue (paramId (pMaster));
seqLenParam = apvts.getRawParameterValue (paramId (pSeqLen));
seqRateParam = apvts.getRawParameterValue (paramId (pSeqRate));
seqRootParam = apvts.getRawParameterValue (paramId (pSeqRoot));
seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave));
seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing));
seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen));
setDefaultPattern();
}
juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, 3, 2));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 3, 3));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDetune),
"Osc 2 Detune", juce::NormalisableRange<float> (-100.0f, 100.0f), 0.0f,
juce::AudioParameterFloatAttributes().withLabel ("ct")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMix),
"Osc Mix", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pCutoff),
"Filter Cutoff", juce::NormalisableRange<float> (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f,
juce::AudioParameterFloatAttributes().withLabel ("Hz")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRes),
"Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAttack),
"Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDecay),
"Decay", juce::NormalisableRange<float> (0.001f, 3.0f, 0.001f, 0.3f), 0.3f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSustain),
"Sustain", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRelease),
"Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pGlide),
"Glide", juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5f), 0.12f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMaster),
"Master", juce::NormalisableRange<float> (0.0f, 1.0f), 0.9f));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pSeqLen), "Pattern Length", 1, 16, 16));
juce::StringArray rates { "1/4", "1/8", "1/8T", "1/16", "1/32" };
layout.add (std::make_unique<juce::AudioParameterChoice> (paramId (pSeqRate), "Rate", rates, 3));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pSeqRoot), "Root", -24, 24, 0));
juce::StringArray octaves { "-2", "-1", "0", "+1", "+2" };
layout.add (std::make_unique<juce::AudioParameterChoice> (paramId (pSeqOctave), "Octave", octaves, 3));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqSwing),
"Swing", juce::NormalisableRange<float> (0.0f, 0.6f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
return layout;
}
void MonoStepAudioProcessor::setDefaultPattern()
{
struct Def { int step; bool gate; int semitone; bool slide; };
const Def defs[] =
{
{ 0, true, 0, false },
{ 1, true, 0, true },
{ 2, true, 3, false },
{ 4, true, 5, false },
{ 5, true, 3, true },
{ 6, true, 0, false },
{ 8, true, 7, false },
{ 9, true, 5, true },
{ 10, true, 3, false },
{ 12, true, 0, false },
{ 14, true, -2, true },
};
for (const auto& d : defs)
{
sequencer.step (d.step).gate = d.gate;
sequencer.step (d.step).semitone = d.semitone;
sequencer.step (d.step).slide = d.slide;
}
}
void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
{
sampleRate = sr;
voice.prepare (sr);
scratch.setSize (1, samplesPerBlock);
lastStep = -1;
lastGateApplied = false;
lastFreqApplied = -1.0f;
freePpq = 0.0;
wasPlaying = false;
lastHostPpq = -1.0;
}
void MonoStepAudioProcessor::releaseResources()
{
voice.reset();
}
void MonoStepAudioProcessor::updateVoiceParams()
{
monostep::SynthVoice::Params p;
p.waveA = (monostep::Waveform) (int) osc1WaveParam->load();
p.waveB = (monostep::Waveform) (int) osc2WaveParam->load();
p.detuneRatio = std::pow (2.0f, detuneParam->load() / 1200.0f);
p.mix = mixParam->load();
p.cutoff = cutoffParam->load();
p.resonance = resParam->load();
p.attack = attackParam->load();
p.decay = decayParam->load();
p.sustain = sustainParam->load();
p.release = releaseParam->load();
p.glide = glideParam->load();
p.master = masterParam->load();
voice.setParams (p);
}
float MonoStepAudioProcessor::midiToFreq (float note) const
{
return 440.0f * std::pow (2.0f, (note - 69.0f) / 12.0f);
}
void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
const int numSamples = buffer.getNumSamples();
const int numChannels = buffer.getNumChannels();
buffer.clear();
if (numSamples == 0)
return;
updateVoiceParams();
const int rateIdx = juce::roundToInt (seqRateParam->load());
const int root = juce::roundToInt (seqRootParam->load());
const int octave = juce::roundToInt (seqOctaveParam->load()) - 2;
const float swing = seqSwingParam->load();
const float stepLen = sequencer.stepLenPpq (rateIdx);
const int seqLen = juce::roundToInt (seqLenParam->load());
const float cyclePpq = (float) seqLen * stepLen;
// --- MIDI notes (gate/trigger for the sequencer) -------------------------
for (const auto metadata : midiMessages)
{
const auto msg = metadata.getMessage();
if (msg.isNoteOn())
{
midiHeld = true;
lastMidiNote = msg.getNoteNumber();
}
else if (msg.isNoteOff())
{
if (msg.getNoteNumber() == lastMidiNote || ! midiHeld)
midiHeld = false;
}
}
// --- transport / clock --------------------------------------------------
bool hostPlaying = false;
double hostPpq = 0.0;
double bpm = 120.0;
if (auto* playHead = getPlayHead())
if (auto pos = playHead->getPosition())
{
hostPlaying = pos->getIsPlaying();
if (auto ppq = pos->getPpqPosition())
hostPpq = *ppq;
if (auto tempo = pos->getBpm())
bpm = *tempo;
}
const double ppqPerSample = (bpm / 60.0) / sampleRate;
// The sequencer only runs while a MIDI note is held. When the host
// transport is running we follow it, otherwise we free-run at the
// fallback BPM so the pattern still steps when there is no DAW clock.
if (midiHeld)
{
if (hostPlaying)
{
if (lastHostPpq >= 0.0 && hostPpq < lastHostPpq - 1.0)
{
lastStep = -1;
lastGateApplied = false;
}
freePpq = hostPpq;
}
else
{
if (wasPlaying)
{
lastStep = -1;
lastGateApplied = false;
}
freePpq += ppqPerSample * numSamples;
}
}
else
{
lastStep = -1;
lastGateApplied = false;
playStep.store (-1);
}
wasPlaying = hostPlaying;
lastHostPpq = hostPpq;
// --- sequencer step -----------------------------------------------------
bool seqGate = false;
float seqFreq = 0.0f;
bool seqSlide = false;
if (midiHeld)
{
const float gateLen = seqGateLenParam->load();
float ppqInCycle = (float) std::fmod (freePpq, (double) cyclePpq);
if (ppqInCycle < 0.0f)
ppqInCycle += cyclePpq;
int stepIdx = 0;
float stepStart = 0.0f;
for (int i = 0; i < seqLen; ++i)
{
const float start = i * stepLen + ((i % 2) ? swing * stepLen : 0.0f);
const float nextStart = (i + 1) * stepLen + (((i + 1) % 2) ? swing * stepLen : 0.0f);
if (ppqInCycle >= start && ppqInCycle < nextStart)
{
stepIdx = i;
stepStart = start;
break;
}
}
lastStep = stepIdx;
playStep.store (stepIdx);
const auto& s = sequencer.step (stepIdx);
seqSlide = s.slide;
// gate length: the step's note sustains for gateLen of the step
const float ppqWithinStep = ppqInCycle - stepStart;
seqGate = s.gate && (ppqWithinStep < stepLen * gateLen);
if (seqGate)
{
// the held MIDI note transposes the pattern; root/octave shift it further
const float note = (float) lastMidiNote + (float) root + octave * 12.0f + s.semitone;
seqFreq = midiToFreq (note + s.cents / 100.0f);
}
}
// --- decide voice event ---------------------------------------------------
bool wantGate = false;
float wantFreq = 0.0f;
bool wantLegato = false;
if (seqGate)
{
wantGate = true;
wantFreq = seqFreq;
wantLegato = seqSlide;
}
if (wantGate != lastGateApplied)
{
if (wantGate)
voice.noteOn (wantFreq, wantLegato);
else
voice.noteOff();
lastGateApplied = wantGate;
lastFreqApplied = wantGate ? wantFreq : -1.0f;
}
else if (wantGate && std::fabs (wantFreq - lastFreqApplied) > 0.01f)
{
voice.noteOn (wantFreq, wantLegato);
lastFreqApplied = wantFreq;
}
(void) lastStep;
// --- render ---------------------------------------------------------------
voice.render (scratch, numSamples);
for (int ch = 0; ch < numChannels; ++ch)
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
}
void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
sequencer.step (idx).gate = gate;
if (onPatternChanged)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
sequencer.step (idx).semitone = juce::jlimit (monostep::minSemitone, monostep::maxSemitone, semitone);
if (onPatternChanged)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepCents (int idx, float cents)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
sequencer.step (idx).cents = juce::jlimit (-50.0f, 50.0f, cents);
if (onPatternChanged)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
sequencer.step (idx).slide = slide;
if (onPatternChanged)
onPatternChanged();
}
juce::String MonoStepAudioProcessor::getStepNoteName (int idx) const
{
const int root = juce::roundToInt (seqRootParam->load());
const int octave = juce::roundToInt (seqOctaveParam->load()) - 2;
int note = root + octave * 12 + sequencer.step (idx).semitone;
if (lastMidiNote >= 0)
note += lastMidiNote;
return juce::MidiMessage::getMidiNoteName (note, true, true, 3);
}
void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
{
juce::String gates;
juce::String notes;
juce::String cents;
juce::String slides;
for (int i = 0; i < monostep::numSteps; ++i)
{
const auto& s = sequencer.step (i);
gates += s.gate ? "1" : "0";
notes += juce::String (s.semitone) + ",";
cents += juce::String (s.cents, 2) + ",";
slides += s.slide ? "1" : "0";
}
seq.setProperty ("gates", gates, nullptr);
seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr);
seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr);
seq.setProperty ("slides", slides, nullptr);
}
void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
{
const auto splitFloats = [] (const juce::String& text)
{
juce::StringArray tokens;
tokens.addTokens (text, ",", "");
juce::Array<float> result;
for (const auto& t : tokens)
result.add (t.getFloatValue());
return result;
};
const auto gates = seq.getProperty ("gates").toString();
const auto notes = splitFloats (seq.getProperty ("notes").toString());
const auto cents = splitFloats (seq.getProperty ("cents").toString());
const auto slides = seq.getProperty ("slides").toString();
for (int i = 0; i < monostep::numSteps; ++i)
{
auto& s = sequencer.step (i);
if (i < gates.length())
s.gate = gates[i] == '1';
if (i < notes.size())
s.semitone = juce::roundToInt (notes[i]);
if (i < cents.size())
s.cents = cents[i];
if (i < slides.length())
s.slide = slides[i] == '1';
}
}
void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::ValueTree state = apvts.state.createCopy();
juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
storeSequence (seq);
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
}
void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml (juce::AudioProcessor::getXmlFromBinary (data, sizeInBytes));
if (xml == nullptr)
return;
const juce::ValueTree state = juce::ValueTree::fromXml (*xml);
apvts.replaceState (state);
if (auto seq = state.getChildWithName ("SEQ"); seq.isValid())
loadSequence (seq);
if (onPatternChanged)
onPatternChanged();
}
juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
{
#ifndef MONOSTEP_HEADLESS
return new MonoStepAudioProcessorEditor (*this);
#else
return nullptr;
#endif
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new MonoStepAudioProcessor();
}

98
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,98 @@
#pragma once
#include <JuceHeader.h>
#include "dsp/SynthVoice.h"
#include "dsp/StepSequencer.h"
class MonoStepAudioProcessor final : public juce::AudioProcessor
{
public:
MonoStepAudioProcessor();
~MonoStepAudioProcessor() override = default;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "MonoStep"; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram (int) override {}
const juce::String getProgramName (int) override { return {}; }
void changeProgramName (int, const juce::String&) override {}
void getStateInformation (juce::MemoryBlock&) override;
void setStateInformation (const void*, int) override;
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
monostep::StepSequencer& getSequencer() { return sequencer; }
const monostep::StepSequencer& getSequencer() const { return sequencer; }
int getNumSteps() const { return monostep::numSteps; }
int getPatternLength() const { return juce::roundToInt (seqLenParam->load()); }
int getCurrentStep() const { return playStep.load(); }
void setStepGate (int idx, bool gate);
void setStepNote (int idx, int semitone);
void setStepCents (int idx, float cents);
void setStepSlide (int idx, bool slide);
juce::String getStepNoteName (int idx) const;
std::function<void()> onPatternChanged;
private:
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
void updateVoiceParams();
float midiToFreq (float note) const;
void storeSequence (juce::ValueTree& seq) const;
void loadSequence (const juce::ValueTree& seq);
void setDefaultPattern();
juce::AudioProcessorValueTreeState apvts;
std::atomic<float>* osc1WaveParam = nullptr;
std::atomic<float>* osc2WaveParam = nullptr;
std::atomic<float>* detuneParam = nullptr;
std::atomic<float>* mixParam = nullptr;
std::atomic<float>* cutoffParam = nullptr;
std::atomic<float>* resParam = nullptr;
std::atomic<float>* attackParam = nullptr;
std::atomic<float>* decayParam = nullptr;
std::atomic<float>* sustainParam = nullptr;
std::atomic<float>* releaseParam = nullptr;
std::atomic<float>* glideParam = nullptr;
std::atomic<float>* masterParam = nullptr;
std::atomic<float>* seqLenParam = nullptr;
std::atomic<float>* seqRateParam = nullptr;
std::atomic<float>* seqRootParam = nullptr;
std::atomic<float>* seqOctaveParam = nullptr;
std::atomic<float>* seqSwingParam = nullptr;
std::atomic<float>* seqGateLenParam = nullptr;
monostep::StepSequencer sequencer;
monostep::SynthVoice voice;
juce::AudioBuffer<float> scratch;
double sampleRate = 44100.0;
int lastStep = -1;
std::atomic<int> playStep { -1 };
bool lastGateApplied = false;
float lastFreqApplied = -1.0f;
bool midiHeld = false;
int lastMidiNote = -1;
bool wasPlaying = false;
double lastHostPpq = -1.0;
double freePpq = 0.0;
};

44
Source/Theme.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include <JuceHeader.h>
#include <cmath>
namespace monostep
{
namespace colours
{
inline const juce::Colour bg (0xFF131519);
inline const juce::Colour panel (0xFF1C1F25);
inline const juce::Colour grid (0xFF262B33);
inline const juce::Colour gridLight (0xFF2E343E);
inline const juce::Colour text (0xFFC9CDD4);
inline const juce::Colour textDim (0xFF6E7683);
inline const juce::Colour accent (0xFFE8A33D);
inline const juce::Colour accent2 (0xFF7FA6FF);
inline const juce::Colour accentDim (0xFF7A5A26);
inline const juce::Colour activeCell (0xFF3A6EA5);
inline const juce::Colour selected (0xFFF0F4F8);
inline const juce::Colour playMarker (0xFFE5484D);
}
inline const juce::Font font (float height)
{
return juce::Font (juce::FontOptions (height));
}
inline juce::String noteNameForOffset (int semitone)
{
const int note = 48 + semitone;
return juce::MidiMessage::getMidiNoteName (note, true, true, 3);
}
inline juce::String formatCents (float cents)
{
if (std::fabs (cents) < 0.5f)
return "0 ct";
return juce::String (cents, 0) + " ct";
}
} // namespace monostep

View file

@ -0,0 +1,56 @@
#pragma once
#include <array>
#include <cmath>
namespace monostep
{
static constexpr int numSteps = 16;
static constexpr int minSemitone = -12;
static constexpr int maxSemitone = 12;
static constexpr int numRows = maxSemitone - minSemitone + 1;
struct Step
{
bool gate = false;
int semitone = 0; // -12 .. +12
float cents = 0.0f; // -50 .. +50
bool slide = false;
};
class StepSequencer
{
public:
Step& step (int i) { return steps[i]; }
const Step& step (int i) const { return steps[i]; }
void clear()
{
for (auto& s : steps)
s = Step{};
}
int stepsPerBeatFromRate (int rateIndex) const
{
// 0: 1/4, 1: 1/8, 2: 1/8T, 3: 1/16, 4: 1/32
switch (rateIndex)
{
case 0: return 1;
case 1: return 2;
case 2: return 3;
case 4: return 8;
default: return 4;
}
}
float stepLenPpq (int rateIndex) const
{
return 1.0f / (float) stepsPerBeatFromRate (rateIndex);
}
private:
std::array<Step, numSteps> steps;
};
} // namespace monostep

189
Source/dsp/SynthVoice.h Normal file
View file

@ -0,0 +1,189 @@
#pragma once
#include <JuceHeader.h>
#include "WaveTables.h"
namespace monostep
{
class SynthVoice
{
public:
struct Params
{
Waveform waveA = Waveform::saw;
Waveform waveB = Waveform::square;
float detuneRatio = 1.0f;
float mix = 0.5f;
float cutoff = 7000.0f;
float resonance = 0.15f;
float attack = 0.005f;
float decay = 0.3f;
float sustain = 0.7f;
float release = 0.4f;
float glide = 0.12f;
float master = 0.9f;
};
void prepare (double sr)
{
sampleRate = (float) sr;
reset();
}
void reset()
{
phaseA = phaseB = 0.0f;
currentFreq = targetFreq = 440.0f;
smoothing = 0.0f;
filterLow = filterBand = 0.0f;
env = 0.0f;
envStage = Stage::idle;
gate = false;
}
void setParams (const Params& p) { params = p; }
bool isActive() const { return gate; }
void noteOn (float freqHz, bool legato)
{
targetFreq = freqHz;
if (gate && legato)
{
const float time = std::max (params.glide, 0.001f);
smoothing = 1.0f - std::exp (-1.0f / (time * sampleRate));
}
else
{
currentFreq = targetFreq;
smoothing = 0.0f;
retriggerEnvelope();
}
gate = true;
}
void noteOff()
{
if (gate)
{
gate = false;
if (envStage != Stage::idle && envStage != Stage::release)
envStage = Stage::release;
}
}
void render (juce::AudioBuffer<float>& buffer, int numSamples)
{
auto* out = buffer.getWritePointer (0);
for (int i = 0; i < numSamples; ++i)
out[i] = renderSample();
}
private:
enum class Stage { idle, attack, decay, sustain, release };
void retriggerEnvelope()
{
envStage = Stage::attack;
}
void updateEnvelope()
{
const float sr = sampleRate;
switch (envStage)
{
case Stage::attack:
env += 1.0f / (params.attack * sr);
if (env >= 1.0f) { env = 1.0f; envStage = Stage::decay; }
break;
case Stage::decay:
env -= (1.0f - params.sustain) / (params.decay * sr);
if (env <= params.sustain) { env = params.sustain; envStage = Stage::sustain; }
break;
case Stage::sustain:
env = params.sustain;
break;
case Stage::release:
env -= 1.0f / (params.release * sr);
if (env <= 0.0f) { env = 0.0f; envStage = Stage::idle; }
break;
case Stage::idle:
break;
}
}
void updateGlide()
{
if (smoothing > 0.0f)
{
currentFreq += (targetFreq - currentFreq) * smoothing;
if (std::fabs (targetFreq - currentFreq) < 0.01f)
{
currentFreq = targetFreq;
smoothing = 0.0f;
}
}
}
float processFilter (float input)
{
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * params.cutoff / sampleRate);
const float q = 1.0f / (1.0f + params.resonance * 9.0f);
filterLow += f * filterBand;
const float high = input - filterLow - q * filterBand;
filterBand += f * high;
return filterLow;
}
float renderSample()
{
updateEnvelope();
updateGlide();
const float incA = currentFreq / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
const float a = renderWave (params.waveA, phaseA, incA);
const float b = renderWave (params.waveB, phaseB, incB);
phaseA = frac (phaseA + incA);
phaseB = frac (phaseB + incB);
float out = (1.0f - params.mix) * a + params.mix * b;
out = processFilter (out);
return out * env * params.master;
}
float sampleRate = 44100.0f;
Params params;
float phaseA = 0.0f;
float phaseB = 0.0f;
float currentFreq = 440.0f;
float targetFreq = 440.0f;
float smoothing = 0.0f;
float filterLow = 0.0f;
float filterBand = 0.0f;
float env = 0.0f;
Stage envStage = Stage::idle;
bool gate = false;
};
} // namespace monostep

71
Source/dsp/WaveTables.h Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include <cmath>
#include <algorithm>
namespace monostep
{
enum class Waveform : int
{
sine = 0,
triangle = 1,
saw = 2,
square = 3
};
inline float frac (float x)
{
return x - std::floor (x);
}
inline float polyBlep (float t, float dt)
{
if (t < dt)
{
t /= dt;
return t + t - t * t - 1.0f;
}
if (t > 1.0f - dt)
{
t = (t - 1.0f) / dt;
return t * t + t + t + 1.0f;
}
return 0.0f;
}
inline float renderWave (Waveform w, float phase, float inc)
{
switch (w)
{
case Waveform::sine:
return std::sin (phase * 6.283185307179586f);
case Waveform::triangle:
{
const float f = frac (phase);
return 4.0f * std::min (f, 1.0f - f) - 1.0f;
}
case Waveform::saw:
{
const float f = frac (phase);
return (2.0f * f - 1.0f) - polyBlep (f, inc);
}
case Waveform::square:
{
const float f = frac (phase);
float v = (f < 0.5f) ? 1.0f : -1.0f;
v += polyBlep (f, inc);
v -= polyBlep (frac (f + 0.5f), inc);
return v;
}
}
return 0.0f;
}
} // namespace monostep

View file

@ -0,0 +1,412 @@
#include "SequencerMatrix.h"
#include "../PluginProcessor.h"
#include "../Theme.h"
using namespace monostep;
SequencerMatrix::SequencerMatrix (MonoStepAudioProcessor& processorRef)
: processor (processorRef)
{
}
int SequencerMatrix::rowForSemitone (int semitone) const
{
return maxSemitone - semitone;
}
int SequencerMatrix::semitoneForRow (int row) const
{
return maxSemitone - row;
}
juce::Rectangle<int> SequencerMatrix::matrixBounds() const
{
const int gridW = juce::roundToInt (cellW * numSteps);
const int gridH = juce::roundToInt (cellH * (numRows + 1));
const int x = juce::roundToInt (labelW + (getWidth() - labelW - gridW) / 2.0f);
const int y = juce::roundToInt (headerH + (getHeight() - headerH - gridH) / 2.0f);
return { x, y, gridW, gridH };
}
void SequencerMatrix::setSelectedStep (int stepIdx)
{
if (stepIdx < 0 || stepIdx >= numSteps)
return;
selectedStep = stepIdx;
repaint();
}
void SequencerMatrix::setPlayPosition (int stepIdx)
{
if (stepIdx < -1 || stepIdx >= numSteps)
return;
if (stepIdx == playPos)
return;
playPos = stepIdx;
repaint();
}
void SequencerMatrix::paint (juce::Graphics& g)
{
g.fillAll (colours::panel);
const auto bounds = matrixBounds();
const float ox = (float) bounds.getX();
const float oy = (float) bounds.getY();
// grid background
g.setColour (colours::bg);
g.fillRect (bounds.toFloat());
// selected column highlight
g.setColour (colours::selected.withAlpha (0.07f));
g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight());
// grid lines
g.setColour (colours::grid);
for (int i = 0; i <= numSteps; ++i)
{
const float x = ox + i * cellW;
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight());
}
for (int i = 0; i <= numRows + 1; ++i)
{
const float y = oy + i * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
}
// accent C rows (semitone multiple of 12)
for (int r = 0; r < numRows; ++r)
{
const int st = semitoneForRow (r);
if (st % 12 == 0)
{
g.setColour (colours::gridLight);
const float y = oy + r * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
}
}
// step header numbers + selected marker
const int seqLen = processor.getPatternLength();
for (int i = 0; i < numSteps; ++i)
{
const float cx = ox + i * cellW + cellW * 0.5f;
const float cy = oy - headerH * 0.5f;
g.setColour ((i % 4 == 0 && i < seqLen) ? colours::text : colours::textDim);
g.setFont (font (10.0f));
g.drawText (juce::String (i + 1), juce::roundToInt (cx - 8.0f), juce::roundToInt (cy - 6.0f), 16, 12, juce::Justification::centred);
if (i == selectedStep)
{
g.setColour (colours::accent);
juce::Path tri;
tri.addTriangle (cx - 4.0f, cy - 8.0f, cx + 4.0f, cy - 8.0f, cx, cy);
g.fillPath (tri);
}
}
// pitch labels
for (int r = 0; r < numRows; ++r)
{
const int st = semitoneForRow (r);
const float ly = oy + r * cellH + cellH * 0.5f;
g.setFont (font (8.5f));
g.setColour (colours::textDim);
g.drawText (juce::String (st > 0 ? "+" : "") + juce::String (st),
juce::roundToInt (labelW * 0.1f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.6f), 10,
juce::Justification::centredRight);
g.setColour (st % 12 == 0 ? colours::accent : colours::text);
g.drawText (noteNameForOffset (st),
juce::roundToInt (labelW * 0.62f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.4f), 10,
juce::Justification::centredRight);
}
// active cells
for (int i = 0; i < numSteps; ++i)
{
const auto& s = processor.getSequencer().step (i);
if (! s.gate)
continue;
const int r = rowForSemitone (s.semitone);
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, oy + r * cellH + 1.5f,
cellW - 3.0f, cellH - 3.0f);
const bool isSelected = (i == selectedStep);
const float radius = 3.0f;
// cell body
juce::ColourGradient grad (isSelected ? colours::accent : colours::activeCell,
cell.getTopLeft(),
isSelected ? colours::accent2 : colours::activeCell.darker (0.6f),
cell.getBottomRight(),
false);
g.setGradientFill (grad);
g.fillRoundedRectangle (cell, radius);
// slide arrow
if (s.slide)
{
const float ax = cell.getX() + cell.getWidth() * 0.5f;
const float ay = cell.getY() + cell.getHeight() - 4.0f;
g.setColour (colours::selected);
juce::Path tri;
tri.addTriangle (ax - 3.5f, ay - 1.5f, ax + 3.5f, ay - 1.5f, ax, ay + 3.5f);
g.fillPath (tri);
}
// fine-tune indicator
if (std::fabs (s.cents) > 0.5f)
{
g.setColour (colours::bg.withAlpha (0.85f));
g.setFont (font (7.5f));
g.drawText (formatCents (s.cents), cell.toNearestInt().reduced (1, 1), juce::Justification::topLeft);
}
}
// --- slide row (bottom row) ----------------------------------------------
const float slideY = oy + numRows * cellH;
g.setColour (colours::accentDim);
g.drawHorizontalLine (juce::roundToInt (slideY - 1.0f), ox, ox + (float) bounds.getWidth());
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("SLIDE",
juce::roundToInt (labelW * 0.1f), juce::roundToInt (slideY + cellH * 0.5f - 6.0f),
juce::roundToInt (labelW * 0.8f), 12,
juce::Justification::centredRight);
for (int i = 0; i < numSteps; ++i)
{
if (! processor.getSequencer().step (i).slide)
continue;
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, slideY + 1.5f,
cellW - 3.0f, cellH - 3.0f);
g.setColour (colours::accent);
g.fillRoundedRectangle (cell, 3.0f);
const float ax = cell.getX() + cell.getWidth() * 0.5f;
const float ay = cell.getY() + cell.getHeight() * 0.5f;
g.setColour (colours::bg);
juce::Path tri;
tri.addTriangle (ax - 3.0f, ay - 2.0f, ax - 3.0f, ay + 2.0f, ax + 3.5f, ay);
g.fillPath (tri);
}
// --- dim columns beyond the pattern length ---------------------------------
if (seqLen < numSteps)
{
g.setColour (colours::bg.withAlpha (0.55f));
g.fillRect (ox + seqLen * cellW, oy,
(numSteps - seqLen) * cellW, (float) bounds.getHeight());
}
// --- play position marker ---------------------------------------------------
if (playPos >= 0)
{
const float px = ox + playPos * cellW + cellW * 0.5f;
g.setColour (colours::playMarker.withAlpha (0.65f));
g.drawVerticalLine (juce::roundToInt (px - 1.0f), oy, oy + (float) bounds.getHeight());
g.drawVerticalLine (juce::roundToInt (px), oy, oy + (float) bounds.getHeight());
g.setColour (colours::playMarker);
juce::Path tri;
tri.addTriangle (px - 3.0f, oy - headerH + 2.0f, px + 3.0f, oy - headerH + 2.0f, px, oy - 1.0f);
g.fillPath (tri);
}
}
SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) const
{
const auto bounds = matrixBounds();
const int col = (int) std::floor ((e.getPosition().getX() - bounds.getX()) / cellW);
const int row = (int) std::floor ((e.getPosition().getY() - bounds.getY()) / cellH);
const bool hit = bounds.contains (e.getPosition())
&& col >= 0 && col < numSteps
&& row >= 0 && row < numRows + 1;
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows, row), hit };
}
void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
{
const auto cell = cellAt (e);
if (! cell.hit)
return;
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
if (cell.row == numRows)
{
slideDragging = true;
slideLastStep = cell.step;
startedSlideActive = processor.getSequencer().step (cell.step).slide;
if (onSlideChanged)
onSlideChanged (cell.step, ! startedSlideActive);
repaint();
return;
}
if (e.mods.isAltDown())
{
fineDragging = true;
fineRefRow = cell.row;
fineStartCents = processor.getSequencer().step (cell.step).cents;
fineLastCents = -1000.0f;
repaint();
return;
}
dragging = true;
moved = false;
dragStep = cell.step;
lastRow = cell.row;
const auto& s = processor.getSequencer().step (cell.step);
startedActive = (s.gate && s.semitone == semitoneForRow (cell.row));
}
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
{
if (fineDragging)
{
const auto cell = cellAt (e);
if (! cell.hit)
return;
// one matrix row (one semitone) maps to 4 cents of fine tuning
const float cents = juce::jlimit (-50.0f, 50.0f,
fineStartCents + (fineRefRow - cell.row) * 4.0f);
if (std::fabs (cents - fineLastCents) >= 0.5f)
{
fineLastCents = cents;
if (onFineChanged)
onFineChanged (selectedStep, cents);
repaint();
}
return;
}
if (slideDragging)
{
const auto cell = cellAt (e);
if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
{
slideLastStep = cell.step;
if (onSlideChanged)
onSlideChanged (cell.step, ! startedSlideActive);
repaint();
}
return;
}
if (! dragging)
return;
const auto cell = cellAt (e);
if (cell.hit && cell.row < numRows && cell.row != lastRow)
{
lastRow = cell.row;
moved = true;
const int pitch = semitoneForRow (cell.row);
if (startedActive)
{
if (onNoteChanged)
onNoteChanged (dragStep, pitch);
}
else
{
if (onGateChanged)
onGateChanged (dragStep, true);
if (onNoteChanged)
onNoteChanged (dragStep, pitch);
}
repaint();
}
}
void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
{
if (fineDragging)
{
fineDragging = false;
return;
}
if (slideDragging)
{
slideDragging = false;
return;
}
if (! dragging)
return;
dragging = false;
const auto cell = cellAt (e);
if (! cell.hit || cell.step != dragStep || cell.row >= numRows)
return;
const int pitch = semitoneForRow (cell.row);
if (startedActive && ! moved)
{
if (onGateChanged)
onGateChanged (cell.step, false);
}
else if (! startedActive && ! moved)
{
if (onGateChanged)
onGateChanged (cell.step, true);
if (onNoteChanged)
onNoteChanged (cell.step, pitch);
}
}

View file

@ -0,0 +1,67 @@
#pragma once
#include <JuceHeader.h>
class MonoStepAudioProcessor;
class SequencerMatrix final : public juce::Component
{
public:
explicit SequencerMatrix (MonoStepAudioProcessor& processor);
~SequencerMatrix() override = default;
std::function<void (int stepIdx)> onStepSelected;
std::function<void (int stepIdx, bool gate)> onGateChanged;
std::function<void (int stepIdx, int semitone)> onNoteChanged;
std::function<void (int stepIdx, float cents)> onFineChanged;
std::function<void (int stepIdx, bool slide)> onSlideChanged;
void setSelectedStep (int stepIdx);
int getSelectedStep() const { return selectedStep; }
void setPlayPosition (int stepIdx);
void paint (juce::Graphics&) override;
void mouseDown (const juce::MouseEvent&) override;
void mouseDrag (const juce::MouseEvent&) override;
void mouseUp (const juce::MouseEvent&) override;
private:
struct CellRect
{
int step;
int row;
bool hit;
};
CellRect cellAt (const juce::MouseEvent&) const;
juce::Rectangle<int> matrixBounds() const;
int rowForSemitone (int semitone) const;
int semitoneForRow (int row) const;
MonoStepAudioProcessor& processor;
float cellW = 24.0f;
float cellH = 17.0f;
float labelW = 40.0f;
float headerH = 24.0f;
float headerPad = 10.0f;
int selectedStep = 0;
int playPos = -1;
bool dragging = false;
bool startedActive = false;
bool moved = false;
int lastRow = 0;
int dragStep = 0;
bool fineDragging = false;
int fineRefRow = 0;
float fineStartCents = 0.0f;
float fineLastCents = -1000.0f;
bool slideDragging = false;
bool startedSlideActive = false;
int slideLastStep = -1;
};

235
tests/TestHost.cpp Normal file
View file

@ -0,0 +1,235 @@
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include <cmath>
static double measureRms (const juce::AudioBuffer<float>& buffer)
{
double sum = 0.0;
int samples = 0;
for (int c = 0; c < buffer.getNumChannels(); ++c)
for (int i = 0; i < buffer.getNumSamples(); ++i)
{
const float s = buffer.getSample (c, i);
sum += (double) s * s;
++samples;
}
return std::sqrt (sum / (double) std::max (1, samples));
}
static bool patternsEqual (const monostep::StepSequencer& a, const monostep::StepSequencer& b)
{
for (int i = 0; i < monostep::numSteps; ++i)
{
const auto& sa = a.step (i);
const auto& sb = b.step (i);
if (sa.gate != sb.gate || sa.semitone != sb.semitone
|| std::fabs (sa.cents - sb.cents) > 0.001f || sa.slide != sb.slide)
return false;
}
return true;
}
// Part A: verify the actual .vst3 binary loads as a VST3 module.
static int testVst3Binary (const juce::File& pluginFile)
{
juce::VST3PluginFormatHeadless format;
juce::OwnedArray<juce::PluginDescription> types;
format.findAllTypesForFile (types, pluginFile.getFullPathName());
if (types.isEmpty())
{
std::cout << "FAILED: VST3 binary did not scan as a valid VST3 plugin\n";
return 1;
}
std::cout << "VST3 scan OK - found " << types.size() << " type(s): "
<< types[0]->name << " (uid " << juce::String::toHexString ((int) types[0]->uniqueId) << ")\n";
if (! format.doesPluginStillExist (*types[0]))
{
std::cout << "FAILED: plugin file does not exist per format check\n";
return 1;
}
return 0;
}
// Part B: drive the actual processor source and render audio.
static int testProcessorRendering()
{
MonoStepAudioProcessor processor;
processor.prepareToPlay (44100.0, 512);
const int totalSamples = (int) (44100.0 * 8.0);
const int blockSize = 512;
const int numBlocks = totalSamples / blockSize;
juce::AudioBuffer<float> block (2, blockSize);
juce::AudioBuffer<float> out (2, totalSamples);
juce::MidiBuffer midi;
// A held MIDI note is what gates the sequencer into running.
midi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
processor.processBlock (block, midi);
for (int c = 0; c < 2; ++c)
out.copyFrom (c, b * blockSize, block, c, 0, blockSize);
}
const double rms = measureRms (out);
std::cout << "Rendered " << totalSamples << " samples, RMS = " << rms << "\n";
if (rms < 1e-4)
{
std::cout << "FAILED: output is silent\n";
return 1;
}
// Without a MIDI note the sequencer must stay silent.
MonoStepAudioProcessor idleProcessor;
idleProcessor.prepareToPlay (44100.0, 512);
double idleSum = 0.0;
juce::MidiBuffer emptyMidi;
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
emptyMidi.clear();
idleProcessor.processBlock (block, emptyMidi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
idleSum += (double) block.getSample (c, i) * block.getSample (c, i);
}
if (std::sqrt (idleSum / (double) (numBlocks * blockSize * 2)) > 1e-5)
{
std::cout << "FAILED: sequencer plays without a MIDI trigger\n";
return 1;
}
std::cout << "Silent without MIDI trigger OK\n";
// parameter sanity
std::cout << "Num parameters: " << processor.getNumParameters() << "\n";
// pattern editing + fine tune
processor.setStepGate (0, true);
processor.setStepNote (0, 7);
processor.setStepCents (0, 42.0f);
processor.setStepSlide (1, true);
if (std::fabs (processor.getSequencer().step (0).cents - 42.0f) > 0.001f)
{
std::cout << "FAILED: fine-tune per step did not apply\n";
return 1;
}
// state round trip through a fresh instance
juce::MemoryBlock state;
processor.getStateInformation (state);
std::cout << "State bytes: " << (int) state.getSize() << "\n";
MonoStepAudioProcessor processor2;
processor2.prepareToPlay (44100.0, 512);
processor2.setStateInformation (state.getData(), (int) state.getSize());
for (int i = 0; i < 4; ++i)
{
const auto& a = processor.getSequencer().step (i);
const auto& b = processor2.getSequencer().step (i);
std::cout << "step " << i << ": gate=" << a.gate << "/" << b.gate
<< " note=" << a.semitone << "/" << b.semitone
<< " cents=" << a.cents << "/" << b.cents
<< " slide=" << a.slide << "/" << b.slide << "\n";
}
if (! patternsEqual (processor.getSequencer(), processor2.getSequencer()))
{
std::cout << "FAILED: pattern did not survive state save/load\n";
return 1;
}
std::cout << "State round-trip preserves pattern OK\n";
// pattern length parameter
if (processor.getPatternLength() != 16)
{
std::cout << "FAILED: default pattern length is not 16\n";
return 1;
}
*processor.getAPVTS().getRawParameterValue ("seqLen") = 8.0f;
if (processor.getPatternLength() != 8)
{
std::cout << "FAILED: pattern length did not change\n";
return 1;
}
std::cout << "Pattern length OK\n";
*processor.getAPVTS().getRawParameterValue ("seqLen") = 16.0f;
// master = 0 should silence the output while a note is held
*processor.getAPVTS().getRawParameterValue ("master") = 0.0f;
processor.reset();
processor.prepareToPlay (44100.0, 512);
double silentSum = 0.0;
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
midi.clear();
if (b == 0)
midi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
processor.processBlock (block, midi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
silentSum += (double) block.getSample (c, i) * block.getSample (c, i);
}
if (std::sqrt (silentSum / (double) (numBlocks * blockSize * 2)) > 1e-5)
{
std::cout << "FAILED: master=0 did not silence output\n";
return 1;
}
std::cout << "Master=0 silences output OK\n";
return 0;
}
int main (int argc, char** argv)
{
if (argc < 2)
{
std::cout << "Usage: MonoStepTestHost <plugin.vst3> [out.wav]\n";
return 1;
}
const juce::File pluginFile (argv[1]);
if (testVst3Binary (pluginFile) != 0)
return 1;
if (testProcessorRendering() != 0)
return 1;
std::cout << "ALL TESTS PASSED\n";
return 0;
}