mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
MonoStep: monophonic 2-oscillator step-sequencer synth
This commit is contained in:
commit
e52e5cb161
14 changed files with 2674 additions and 0 deletions
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal 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
97
CMakeLists.txt
Normal 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
48
Source/BuildInfo.h
Normal 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
613
Source/PluginEditor.cpp
Normal 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
190
Source/PluginEditor.h
Normal 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
516
Source/PluginProcessor.cpp
Normal 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
98
Source/PluginProcessor.h
Normal 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
44
Source/Theme.h
Normal 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
|
||||
56
Source/dsp/StepSequencer.h
Normal file
56
Source/dsp/StepSequencer.h
Normal 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
189
Source/dsp/SynthVoice.h
Normal 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
71
Source/dsp/WaveTables.h
Normal 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
|
||||
412
Source/ui/SequencerMatrix.cpp
Normal file
412
Source/ui/SequencerMatrix.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
67
Source/ui/SequencerMatrix.h
Normal file
67
Source/ui/SequencerMatrix.h
Normal 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
235
tests/TestHost.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue