Prototype

This commit is contained in:
Armin 2026-08-05 23:12:17 +02:00
commit 4271fc34bb
14 changed files with 950 additions and 169 deletions

View file

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.22)
project(MonoStep VERSION 1.0.0 LANGUAGES C CXX)
project(Monostep VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -63,32 +63,32 @@ endif()
add_subdirectory("${JUCE_DIR}" juce_build)
juce_add_plugin(MonoStep
juce_add_plugin(Monostep
VERSION 1.0.0
COMPANY_NAME "armin"
BUNDLE_ID "com.armin.monostep"
FORMATS VST3
PRODUCT_NAME "MonoStep"
PRODUCT_NAME "Monostep"
PLUGIN_MANUFACTURER_CODE Armn
PLUGIN_CODE MStp
DESCRIPTION "Monophonic 2-oscillator step-sequencer synth"
EDITOR_NAME MonoStepAudioProcessorEditor
EDITOR_NAME MonostepAudioProcessorEditor
IS_SYNTH TRUE
NEEDS_MIDI_INPUT TRUE
VST3_AUTO_MANIFEST TRUE
)
juce_generate_juce_header(MonoStep)
juce_generate_juce_header(Monostep)
target_sources(MonoStep PRIVATE
target_sources(Monostep PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/ui/SequencerMatrix.cpp
)
target_include_directories(MonoStep PRIVATE Source)
target_include_directories(Monostep PRIVATE Source)
target_compile_definitions(MonoStep PRIVATE
target_compile_definitions(Monostep PRIVATE
JUCE_WEB_BROWSER=0
JUCE_VST3_CAN_REPLACE_VST2=0
MONOSTEP_GIT_INFO="${MONOSTEP_GIT_STRING}"
@ -97,20 +97,20 @@ target_compile_definitions(MonoStep PRIVATE
option(MONOSTEP_BUILD_TESTS "Build the headless host test" ON)
if(MONOSTEP_BUILD_TESTS)
juce_add_console_app(MonoStepTestHost
PRODUCT_NAME MonoStepTestHost
juce_add_console_app(MonostepTestHost
PRODUCT_NAME MonostepTestHost
)
juce_generate_juce_header(MonoStepTestHost)
target_link_libraries(MonoStepTestHost PRIVATE
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
target_include_directories(MonostepTestHost PRIVATE Source)
target_sources(MonostepTestHost PRIVATE
Source/PluginProcessor.cpp
tests/TestHost.cpp
)
target_compile_definitions(MonoStepTestHost PRIVATE
target_compile_definitions(MonostepTestHost PRIVATE
MONOSTEP_HEADLESS=1
JUCE_PLUGINHOST_VST3=1
)

View file

@ -1,10 +1,10 @@
SHELL := /bin/bash
BUILD_DIR := build
PLUGIN_DIR := build/MonoStep_artefacts/Release/VST3
PLUGIN := MonoStep.vst3
PLUGIN_DIR := build/Monostep_artefacts/Release/VST3
PLUGIN := Monostep.vst3
VST3_DIR := $(HOME)/Library/Audio/Plug-Ins/VST3
TEST_HOST := build/MonoStepTestHost_artefacts/Release/MonoStepTestHost
TEST_HOST := build/MonostepTestHost_artefacts/Release/MonostepTestHost
.PHONY: all build install test clean
@ -13,7 +13,7 @@ all: install
# Configure (if needed) and build the VST3 plugin.
build:
cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release
cmake --build $(BUILD_DIR) --target MonoStep_VST3
cmake --build $(BUILD_DIR) --target Monostep_VST3
# Build then copy the fresh plugin into the user VST3 directory.
install: build
@ -23,7 +23,7 @@ install: build
@echo "Installed $(PLUGIN) into $(VST3_DIR)"
test: build
cmake --build $(BUILD_DIR) --target MonoStepTestHost
cmake --build $(BUILD_DIR) --target MonostepTestHost
"$(TEST_HOST)" "$(PLUGIN_DIR)/$(PLUGIN)"
clean:

View file

@ -1,4 +1,4 @@
# MonoStep
# Monostep
A monophonic 2-oscillator step-sequencer synthesizer implemented as a VST3 plugin with JUCE.
@ -10,7 +10,7 @@ A monophonic 2-oscillator step-sequencer synthesizer implemented as a VST3 plugi
- Per-step gates, pitches (+/- 6 semitones), fine tuning (cents), and slide (legato)
- Matrix editor with click-to-toggle steps, drag-to-pitch, and a slide row
- Rotary-knob GUI with zoom (100% - 200%), a step panel with fine-tune, and a
live red play-position marker
live orange play-position highlight
- Footer status bar showing the git commit and build date
- Pattern stored in the plugin state (save/restore with the DAW)
@ -37,13 +37,13 @@ cmake -S . -B build
cmake -S . -B build -DJUCE_DIR=/path/to/juce
# 3. Build the plugin:
cmake --build build --target MonoStep_VST3
cmake --build build --target Monostep_VST3
```
The VST3 plugin is written to:
```
build/MonoStep_artefacts/Release/VST3/MonoStep.vst3
build/Monostep_artefacts/Release/VST3/Monostep.vst3
```
### Quick start with make
@ -58,11 +58,11 @@ make clean # remove the build directory
### Installing (manual copy)
The VST3 plugin ships as a macOS bundle (`MonoStep.vst3`). Copy the whole
The VST3 plugin ships as a macOS bundle (`Monostep.vst3`). Copy the whole
bundle into your DAW's VST3 directory:
```sh
cp -R build/MonoStep_artefacts/Release/VST3/MonoStep.vst3 \
cp -R build/Monostep_artefacts/Release/VST3/Monostep.vst3 \
~/Library/Audio/Plug-Ins/VST3/
```
@ -71,19 +71,19 @@ Notes for macOS:
- The per-user VST3 directory is `~/Library/Audio/Plug-Ins/VST3/`; create it
with `mkdir -p` if it does not exist. A system-wide copy would go to
`/Library/Audio/Plug-Ins/VST3/` but requires `sudo`.
- When updating, remove the old bundle first (`rm -rf` on the `MonoStep.vst3`
- When updating, remove the old bundle first (`rm -rf` on the `Monostep.vst3`
inside the target directory) so stale files do not remain.
- Your DAW may cache the plugin list — quit and relaunch it (or rescan plugins)
after installing before looking for MonoStep.
after installing before looking for Monostep.
## Running the tests
Build the headless host test and run it against the built plugin:
```sh
cmake --build build --target MonoStepTestHost
build/MonoStepTestHost_artefacts/Release/MonoStepTestHost \
build/MonoStep_artefacts/Release/VST3/MonoStep.vst3
cmake --build build --target MonostepTestHost
build/MonostepTestHost_artefacts/Release/MonostepTestHost \
build/Monostep_artefacts/Release/VST3/Monostep.vst3
```
The host scans the VST3, renders a buffer, and verifies that the pattern

View file

@ -6,6 +6,19 @@
using namespace monostep;
namespace
{
juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area)
{
const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f);
const auto bottom = area.getBottomLeft().translated (area.getWidth() * 0.5f, 0.0f);
return juce::ColourGradient (colours::accent, top,
colours::accent.darker (0.35f), bottom,
false);
}
} // namespace
//==============================================================================
EditorLookAndFeel::EditorLookAndFeel()
{
@ -33,6 +46,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
g.setColour (colours::gridLight);
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
// Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look.
constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f;
const float tiltSin = std::sin (-2.0f * degToRad);
const float tiltCos = std::cos (-2.0f * degToRad);
const auto topPoint = centre + juce::Point<float> ( radius * tiltSin, -radius * tiltCos);
const auto bottomPoint = centre + juce::Point<float> (-radius * tiltSin, radius * tiltCos);
juce::ColourGradient shading (juce::Colours::white.withAlpha (0.07f), topPoint,
juce::Colours::black.withAlpha (0.20f), bottomPoint, false);
g.setGradientFill (shading);
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
if (end - start < 6.0f)
{
juce::Path valueArc;
@ -40,19 +66,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
radius * 0.85f, radius * 0.85f,
0.0f, start, angle, true);
g.setColour (colours::accent);
g.setGradientFill (orangeGradient (bounds));
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);
// The pointer reaches out to the value arc so it touches the arc.
const float tickLen = radius * 0.85f + 1.0f;
g.setGradientFill (orangeGradient (bounds));
g.drawLine (centre.getX(), centre.getY(),
centre.getX() + tickLen * std::sin (angle),
centre.getY() - tickLen * std::cos (angle),
2.5f);
g.setColour (colours::accent);
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f);
}
@ -61,7 +87,11 @@ void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton&
{
const auto bounds = button.getLocalBounds().toFloat();
g.setColour (button.getToggleState() ? colours::accent : colours::grid);
if (button.getToggleState())
g.setGradientFill (orangeGradient (bounds));
else
g.setColour (colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
g.setFont (font (11.0f));
@ -75,7 +105,7 @@ void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState())
g.setColour (colours::accent);
g.setGradientFill (orangeGradient (bounds));
else
g.setColour (highlighted ? colours::gridLight : colours::grid);
@ -89,6 +119,81 @@ void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& but
g.drawText (button.getButtonText(), button.getLocalBounds(), juce::Justification::centred);
}
void EditorLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool,
int, int, int, int, juce::ComboBox&)
{
const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2));
g.setColour (colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (colours::gridLight);
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
const float cx = (float) (width - 15);
const float cy = (float) height * 0.5f;
juce::Path arrow;
arrow.addTriangle (cx - 5.0f, cy - 3.0f, cx + 5.0f, cy - 3.0f, cx, cy + 3.5f);
g.setColour (colours::accent);
g.fillPath (arrow);
}
juce::Font EditorLookAndFeel::getComboBoxFont (juce::ComboBox&)
{
return font (11.0f);
}
void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
{
g.setColour (colours::panel);
g.fillRect (0, 0, width, height);
g.setColour (colours::gridLight);
g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f);
}
void EditorLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle<int>& area,
bool isSeparator, bool isActive, bool isHighlighted,
bool isTicked, bool hasSubMenu,
const juce::String& text, const juce::String& shortcutKeyText,
const juce::Drawable* icon, const juce::Colour* textColour)
{
(void) hasSubMenu;
(void) icon;
if (isSeparator)
{
g.setColour (colours::grid);
g.fillRect (area.reduced (10, 0).withY (area.getCentreY()).withHeight (1));
return;
}
g.setColour (isHighlighted ? colours::gridLight : colours::panel);
g.fillRect (area);
if (isTicked)
{
g.setColour (colours::accent);
g.fillEllipse ((float) (area.getRight() - 17), area.getCentreY() - 3.5f, 7.0f, 7.0f);
}
g.setFont (font (11.0f));
g.setColour (isActive ? (textColour != nullptr ? *textColour : colours::text)
: colours::textDim);
g.drawFittedText (text, area.withTrimmedLeft (12).withTrimmedRight (isTicked ? 28 : 10),
juce::Justification::centredLeft, 1);
if (shortcutKeyText.isNotEmpty())
{
g.setColour (colours::textDim);
g.drawFittedText (shortcutKeyText, area.withTrimmedRight (16), juce::Justification::centredRight, 1);
}
}
juce::Font EditorLookAndFeel::getPopupMenuFont()
{
return font (11.0f);
}
//==============================================================================
Knob::Knob (const juce::String& title)
{
@ -124,10 +229,14 @@ Knob::Knob (const juce::String& title)
void Knob::resized()
{
const int w = getWidth();
const int h = getHeight();
titleLabel.setBounds (0, 0, w, 14);
slider.setBounds (4, 15, w - 8, w - 8);
valueLabel.setBounds (0, w - 8 + 15, w, 12);
const int valueH = 12;
const int sliderH = juce::jmax (20, juce::jmin (w - 8, h - 15 - valueH));
slider.setBounds (4, 15, w - 8, sliderH);
valueLabel.setBounds (0, 15 + sliderH, w, valueH);
}
void Knob::updateValueLabel()
@ -234,7 +343,7 @@ void ChoiceBar::resized()
}
//==============================================================================
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
: processor (processorRef),
fineKnob ("Fine")
{
@ -284,6 +393,70 @@ StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
clearButton.onClick = [this] { clearPattern(); };
addAndMakeVisible (clearButton);
shiftLeftButton.setButtonText ("<<");
shiftLeftButton.onClick = [this] { processor.shiftPattern (-1); };
addAndMakeVisible (shiftLeftButton);
shiftRightButton.setButtonText (">>");
shiftRightButton.onClick = [this] { processor.shiftPattern (1); };
addAndMakeVisible (shiftRightButton);
toolsCaption.setText ("TOOLS", juce::dontSendNotification);
toolsCaption.setJustificationType (juce::Justification::centredLeft);
toolsCaption.setFont (font (9.0f));
toolsCaption.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (toolsCaption);
presetCombo.setTextWhenNothingSelected ("Pick a preset");
presetCombo.setColour (juce::ComboBox::textColourId, colours::text);
presetCombo.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
presetCombo.setColour (juce::ComboBox::backgroundColourId, colours::grid);
presetCombo.setColour (juce::ComboBox::arrowColourId, colours::accent);
for (int i = 0; i < processor.getNumPresets(); ++i)
presetCombo.addItem (processor.getPresetName (i), i + 1);
presetCombo.onChange = [this]
{
const int id = presetCombo.getSelectedId();
if (id > 0)
processor.applyPreset (id - 1);
};
addAndMakeVisible (presetCombo);
randomizeButton.setButtonText ("Randomize");
randomizeButton.onClick = [this]
{
if (randomizeToggles[RandomPattern].getToggleState())
processor.randomizePattern();
if (randomizeToggles[RandomOsc].getToggleState())
processor.randomizeOsc();
if (randomizeToggles[RandomFilter].getToggleState())
processor.randomizeFilter();
if (randomizeToggles[RandomEnv].getToggleState())
processor.randomizeEnv();
if (randomizeToggles[RandomSeq].getToggleState())
processor.randomizeSeqSettings();
if (randomizeToggles[RandomFx].getToggleState())
processor.randomizeFx();
};
addAndMakeVisible (randomizeButton);
randomizeCaption.setText ("RANDOMIZE", juce::dontSendNotification);
randomizeCaption.setJustificationType (juce::Justification::centredLeft);
randomizeCaption.setFont (font (9.0f));
randomizeCaption.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (randomizeCaption);
static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX" };
for (int i = 0; i < RandomGroupCount; ++i)
{
randomizeToggles[i].setButtonText (groupNames[i]);
randomizeToggles[i].setToggleState (true, juce::dontSendNotification);
addAndMakeVisible (randomizeToggles[i]);
}
setSelectedStep (0);
}
@ -328,6 +501,7 @@ void StepPanel::clearPattern()
processor.setStepNote (i, 0);
processor.setStepCents (i, 0.0f);
processor.setStepSlide (i, false);
processor.setStepAccent (i, false);
}
refresh();
@ -344,18 +518,37 @@ void StepPanel::resized()
const int knobX = (w - 74) / 2;
const int knobY = 68;
fineKnob.setBounds (knobX, knobY, 74, 74);
fineKnob.setBounds (knobX, knobY, 74, 94);
const int btnY = knobY + 25;
const int btnY = knobY + 102;
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);
hintLabel.setBounds (8, btnY + 30, w - 16, 14);
const int shiftW = 22;
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26);
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26);
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26);
toolsCaption.setBounds (10, btnY + 86, w - 20, 14);
presetCombo.setBounds (10, btnY + 102, w - 20, 26);
randomizeButton.setBounds (10, btnY + 134, w - 20, 26);
randomizeCaption.setBounds (10, btnY + 166, w - 20, 12);
const int colW = (w - 20 - 4) / 2;
const int rowH = 20;
for (int i = 0; i < RandomGroupCount; ++i)
{
const int col = i / 3;
const int row = i % 3;
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
}
}
//==============================================================================
MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor)
MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcessor& processor)
: AudioProcessorEditor (processor),
processorRef (processor),
content (*this),
@ -365,8 +558,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
rootKnob ("Root"),
gateKnob ("Gate"),
masterKnob ("Master"),
coarseKnob ("Coarse"),
coarseKnob2 ("Coarse"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
fineKnob1 ("Fine"),
fineKnob2 ("Fine"),
cutoffKnob ("Cutoff"),
resKnob ("Res"),
glideKnob ("Glide"),
@ -374,6 +571,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
decayKnob ("Decay"),
sustainKnob ("Sustain"),
releaseKnob ("Release"),
driveKnob ("Drive"),
ringKnob ("Ring"),
accentKnob ("Accent"),
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}),
@ -414,6 +614,7 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
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 (coarseKnob, "osc2Coarse", [] (float v) { return juce::String (juce::roundToInt (v)) + " st"; });
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"; });
@ -423,6 +624,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
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"; });
addKnob (driveKnob, "drive", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (ringKnob, "ringmod", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
content.addAndMakeVisible (matrix);
content.addAndMakeVisible (stepPanel);
@ -466,6 +670,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
stepPanel.refresh();
};
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
{
processorRef.setStepAccent (stepIdx, accent);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
@ -473,14 +683,14 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
refreshEditor();
}
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor()
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
void MonoStepAudioProcessorEditor::timerCallback()
void MonostepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
@ -491,27 +701,27 @@ void MonoStepAudioProcessorEditor::timerCallback()
}
}
void MonoStepAudioProcessorEditor::refreshEditor()
void MonostepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g)
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
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.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);
g.drawText ("2 OSC Step-Sequencer Synth", 16, 34, 130, 12, juce::Justification::centredLeft);
const auto drawCaption = [&] (const juce::Component& component, const juce::String& text)
{
@ -538,14 +748,14 @@ void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
}
void MonoStepAudioProcessorEditor::setZoomFactor (float newZoom)
void MonostepAudioProcessorEditor::setZoomFactor (float newZoom)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
void MonoStepAudioProcessorEditor::resized()
void MonostepAudioProcessorEditor::resized()
{
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
@ -563,18 +773,19 @@ void MonoStepAudioProcessorEditor::resized()
const int w = baseWidth;
const int h = baseHeight;
const int topBarH = 58;
const int topBarH = 78;
const int bottomBarH = 96;
const int pad = 12;
const int topBarY = (topBarH - 26) / 2;
lengthKnob.setBounds (128, 2, 54, 56);
rateBar.setBounds (192, 18, 210, 26);
lengthKnob.setBounds (128, 2, 54, 74);
rateBar.setBounds (192, topBarY, 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);
swingKnob.setBounds (422, 2, 54, 74);
rootKnob.setBounds (482, 2, 54, 74);
octaveBar.setBounds (548, topBarY, 170, 26);
zoomBar.setBounds (w - pad - 54 - 8 - 236, topBarY, 236, 26);
masterKnob.setBounds (w - pad - 54, 2, 54, 74);
const int panelW = 200;
const int midTop = topBarH;
@ -592,7 +803,7 @@ void MonoStepAudioProcessorEditor::resized()
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56;
const int knobGap = 72;
const int knobGap = 60;
const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob)
@ -601,6 +812,7 @@ void MonoStepAudioProcessorEditor::resized()
x += knobGap;
};
placeKnob (coarseKnob);
placeKnob (detuneKnob);
placeKnob (mixKnob);
placeKnob (cutoffKnob);
@ -610,4 +822,7 @@ void MonoStepAudioProcessorEditor::resized()
placeKnob (decayKnob);
placeKnob (sustainKnob);
placeKnob (releaseKnob);
placeKnob (driveKnob);
placeKnob (ringKnob);
placeKnob (accentKnob);
}

View file

@ -3,7 +3,7 @@
#include <JuceHeader.h>
#include "ui/SequencerMatrix.h"
class MonoStepAudioProcessor;
class MonostepAudioProcessor;
class EditorLookAndFeel final : public juce::LookAndFeel_V4
{
@ -23,6 +23,17 @@ public:
void drawButtonText (juce::Graphics&, juce::TextButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override;
void drawComboBox (juce::Graphics&, int width, int height, bool isMouseButtonDown,
int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&) override;
juce::Font getComboBoxFont (juce::ComboBox&) override;
void drawPopupMenuBackground (juce::Graphics&, int width, int height) override;
void drawPopupMenuItem (juce::Graphics&, const juce::Rectangle<int>& area,
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked, bool hasSubMenu,
const juce::String& text, const juce::String& shortcutKeyText,
const juce::Drawable* icon, const juce::Colour* textColour) override;
juce::Font getPopupMenuFont() override;
};
class Knob final : public juce::Component
@ -93,7 +104,7 @@ private:
class StepPanel final : public juce::Component
{
public:
explicit StepPanel (MonoStepAudioProcessor& processor);
explicit StepPanel (MonostepAudioProcessor& processor);
void setSelectedStep (int stepIdx);
void refresh();
@ -105,7 +116,7 @@ public:
private:
void clearPattern();
MonoStepAudioProcessor& processor;
MonostepAudioProcessor& processor;
int selectedStep = 0;
bool updating = false;
@ -118,14 +129,34 @@ private:
juce::TextButton nextButton;
juce::Label hintLabel;
juce::TextButton clearButton;
juce::TextButton shiftLeftButton;
juce::TextButton shiftRightButton;
juce::Label toolsCaption;
juce::ComboBox presetCombo;
juce::TextButton randomizeButton;
juce::Label randomizeCaption;
enum RandomGroup
{
RandomPattern,
RandomOsc,
RandomFilter,
RandomEnv,
RandomSeq,
RandomFx,
RandomGroupCount
};
juce::ToggleButton randomizeToggles[RandomGroupCount];
};
class MonoStepAudioProcessorEditor final : public juce::AudioProcessorEditor,
class MonostepAudioProcessorEditor final : public juce::AudioProcessorEditor,
public juce::Timer
{
public:
explicit MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor);
~MonoStepAudioProcessorEditor() override;
explicit MonostepAudioProcessorEditor (MonostepAudioProcessor& processor);
~MonostepAudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
@ -138,15 +169,15 @@ private:
class ContentComponent final : public juce::Component
{
public:
explicit ContentComponent (MonoStepAudioProcessorEditor& owner) : ownerRef (owner) {}
explicit ContentComponent (MonostepAudioProcessorEditor& owner) : ownerRef (owner) {}
void paint (juce::Graphics& g) override { ownerRef.paintContent (g); }
private:
MonoStepAudioProcessorEditor& ownerRef;
MonostepAudioProcessorEditor& ownerRef;
};
MonoStepAudioProcessor& processorRef;
MonostepAudioProcessor& processorRef;
ContentComponent content;
@ -165,8 +196,12 @@ private:
Knob rootKnob;
Knob gateKnob;
Knob masterKnob;
Knob coarseKnob;
Knob coarseKnob2;
Knob detuneKnob;
Knob mixKnob;
Knob fineKnob1;
Knob fineKnob2;
Knob cutoffKnob;
Knob resKnob;
Knob glideKnob;
@ -174,6 +209,9 @@ private:
Knob decayKnob;
Knob sustainKnob;
Knob releaseKnob;
Knob driveKnob;
Knob ringKnob;
Knob accentKnob;
ChoiceBar rateBar;
ChoiceBar octaveBar;

View file

@ -8,30 +8,35 @@ static const char* paramId (int index)
{
static const char* ids[] =
{
"osc1Wave", "osc2Wave", "detune", "mix",
"osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master",
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen"
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
"drive", "ringmod", "accent", "fine1"
};
return ids[index];
}
enum ParamIndex
{
pOsc1Wave, pOsc2Wave, pDetune, pMix,
pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
pDrive, pRingMod, pAccent, pFine1,
numParams
};
MonoStepAudioProcessor::MonoStepAudioProcessor()
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));
osc1CoarseParam = apvts.getRawParameterValue (paramId (pOsc1Coarse));
osc2CoarseParam = apvts.getRawParameterValue (paramId (pOsc2Coarse));
detuneParam = apvts.getRawParameterValue (paramId (pDetune));
mixParam = apvts.getRawParameterValue (paramId (pMix));
osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1));
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
resParam = apvts.getRawParameterValue (paramId (pRes));
attackParam = apvts.getRawParameterValue (paramId (pAttack));
@ -46,16 +51,22 @@ MonoStepAudioProcessor::MonoStepAudioProcessor()
seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave));
seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing));
seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen));
driveParam = apvts.getRawParameterValue (paramId (pDrive));
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
accentParam = apvts.getRawParameterValue (paramId (pAccent));
setDefaultPattern();
}
juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::createParameterLayout()
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::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, 7, 2));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 7, 3));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Coarse), "Osc 1 Coarse", -12, 12, 0));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Coarse), "Osc 2 Coarse", -12, 12, 0));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDetune),
"Osc 2 Detune", juce::NormalisableRange<float> (-100.0f, 100.0f), 0.0f,
@ -64,6 +75,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::crea
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 (pFine1),
"Phase Offset", juce::NormalisableRange<float> (-1.0f, 1.0f), 0.0f,
juce::AudioParameterFloatAttributes().withLabel ("±1")));
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")));
@ -109,10 +124,19 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive),
"Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRingMod),
"Ring Mod", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
"Accent", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
return layout;
}
void MonoStepAudioProcessor::setDefaultPattern()
void MonostepAudioProcessor::setDefaultPattern()
{
struct Def { int step; bool gate; int semitone; bool slide; };
const Def defs[] =
@ -136,9 +160,199 @@ void MonoStepAudioProcessor::setDefaultPattern()
sequencer.step (d.step).semitone = d.semitone;
sequencer.step (d.step).slide = d.slide;
}
sequencer.step (0).accent = true;
sequencer.step (4).accent = true;
sequencer.step (8).accent = true;
}
void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
void MonostepAudioProcessor::applyParamValue (const juce::String& id, float value)
{
if (auto* param = apvts.getParameter (id))
{
param->setValueNotifyingHost (param->getNormalisableRange().convertTo0to1 (value));
apvts.getParameterAsValue (id) = value;
}
}
int MonostepAudioProcessor::getNumPresets() const
{
return 18;
}
const char* MonostepAudioProcessor::getPresetName (int index) const
{
static const char* names[] =
{
"Wet Tape",
"Acid Wash",
"Tin Can",
"Bent Toy",
"Neon Arcade",
"Glitch Garden",
"Underwater FM",
"Robo Funk",
"Lo-Fi Noir",
"Spectral Sweep",
"Broken Radio",
"Plasma Organ",
"Glass Bells",
"Warm Pad",
"Pulse Groove",
"Steel Pluck",
"Sub Rumble",
"Tape Bounce"
};
return names[juce::jlimit (0, getNumPresets() - 1, index)];
}
void MonostepAudioProcessor::applyPreset (int index)
{
struct Preset
{
int osc1; int osc2; int coarse; float detune; float mix;
float cutoff; float res; float attack; float decay; float sustain; float release;
float glide; float gateLen; float swing; int rate; int root; int octave;
float drive; float ring; float accent;
};
static const Preset presets[] =
{
// --- gritty / glidy -----------------------------------------------------
{ 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f }, // Wet Tape
{ 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f }, // Acid Wash
{ 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f }, // Tin Can
{ 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f }, // Bent Toy
{ 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f }, // Broken Radio
{ 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f }, // Neon Arcade
{ 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f }, // Glitch Garden
{ 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f }, // Robo Funk
{ 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f }, // Lo-Fi Noir
{ 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f }, // Spectral Sweep
{ 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f }, // Plasma Organ
{ 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f }, // Underwater FM
// --- clean / no glide / no drive ---------------------------------------
{ 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f }, // Glass Bells
{ 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f }, // Warm Pad
{ 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f }, // Pulse Groove
{ 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f }, // Steel Pluck
{ 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f }, // Sub Rumble
{ 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f }, // Tape Bounce
};
const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)];
applyParamValue ("osc1Wave", (float) pr.osc1);
applyParamValue ("osc2Wave", (float) pr.osc2);
applyParamValue ("osc2Coarse", (float) pr.coarse);
applyParamValue ("detune", pr.detune);
applyParamValue ("mix", pr.mix);
applyParamValue ("cutoff", pr.cutoff);
applyParamValue ("res", pr.res);
applyParamValue ("attack", pr.attack);
applyParamValue ("decay", pr.decay);
applyParamValue ("sustain", pr.sustain);
applyParamValue ("release", pr.release);
applyParamValue ("glide", pr.glide);
applyParamValue ("seqGateLen", pr.gateLen);
applyParamValue ("seqSwing", pr.swing);
applyParamValue ("seqRate", (float) pr.rate);
applyParamValue ("seqRoot", (float) pr.root);
applyParamValue ("seqOctave", (float) pr.octave);
applyParamValue ("drive", pr.drive);
applyParamValue ("ringmod", pr.ring);
applyParamValue ("accent", pr.accent);
}
void MonostepAudioProcessor::randomizePattern()
{
const int length = juce::roundToInt (seqLenParam->load());
int filled = 0;
for (int i = 0; i < monostep::numSteps; ++i)
{
auto& s = sequencer.step (i);
if (i >= length)
{
s.gate = false;
continue;
}
const float chance = (i == 0) ? 0.9f : 0.45f;
s.gate = random.nextFloat() < chance;
if (s.gate)
{
++filled;
s.semitone = random.nextInt (monostep::maxSemitone - monostep::minSemitone + 1) + monostep::minSemitone;
s.cents = random.nextFloat() * 100.0f - 50.0f;
s.slide = random.nextFloat() < 0.25f;
s.accent = random.nextFloat() < 0.3f;
}
}
if (filled == 0)
{
auto& s = sequencer.step (random.nextInt (length));
s.gate = true;
s.semitone = 0;
}
if (onPatternChanged)
onPatternChanged();
}
void MonostepAudioProcessor::randomizeOsc()
{
applyParamValue ("osc1Wave", (float) random.nextInt (4));
applyParamValue ("osc2Wave", (float) random.nextInt (4));
applyParamValue ("osc2Coarse", (float) (random.nextInt (25) - 12));
applyParamValue ("detune", (float) (random.nextInt (101) - 50));
applyParamValue ("mix", 0.2f + random.nextFloat() * 0.6f);
}
void MonostepAudioProcessor::randomizeFilter()
{
applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f));
applyParamValue ("res", random.nextFloat() * 0.8f);
}
void MonostepAudioProcessor::randomizeEnv()
{
applyParamValue ("attack", 0.001f * std::pow (500.0f, random.nextFloat()));
applyParamValue ("decay", 0.05f * std::pow (30.0f, random.nextFloat()));
applyParamValue ("sustain", random.nextFloat());
applyParamValue ("release", 0.05f * std::pow (40.0f, random.nextFloat()));
applyParamValue ("glide", random.nextFloat() * 0.5f);
}
void MonostepAudioProcessor::randomizeSeqSettings()
{
applyParamValue ("seqGateLen", 0.3f + random.nextFloat() * 0.7f);
applyParamValue ("seqSwing", random.nextFloat() * 0.4f);
applyParamValue ("seqRate", (float) random.nextInt (5));
applyParamValue ("seqLen", (float) (random.nextInt (13) + 4));
}
void MonostepAudioProcessor::randomizeFx()
{
applyParamValue ("drive", random.nextFloat());
applyParamValue ("ringmod", random.nextFloat());
}
void MonostepAudioProcessor::randomizeParams()
{
randomizeOsc();
randomizeFilter();
randomizeEnv();
randomizeSeqSettings();
randomizeFx();
}
void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
{
sampleRate = sr;
voice.prepare (sr);
@ -151,17 +365,18 @@ void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
lastHostPpq = -1.0;
}
void MonoStepAudioProcessor::releaseResources()
void MonostepAudioProcessor::releaseResources()
{
voice.reset();
}
void MonoStepAudioProcessor::updateVoiceParams()
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.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f)
* std::pow (2.0f, detuneParam->load() / 1200.0f);
p.mix = mixParam->load();
p.cutoff = cutoffParam->load();
p.resonance = resParam->load();
@ -171,15 +386,17 @@ void MonoStepAudioProcessor::updateVoiceParams()
p.release = releaseParam->load();
p.glide = glideParam->load();
p.master = masterParam->load();
p.drive = driveParam->load();
p.ringMod = ringModParam->load();
voice.setParams (p);
}
float MonoStepAudioProcessor::midiToFreq (float note) const
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)
void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
@ -265,8 +482,12 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
}
else
{
if (lastGateApplied)
voice.noteOff();
lastStep = -1;
lastGateApplied = false;
lastFreqApplied = -1.0f;
playStep.store (-1);
}
@ -277,6 +498,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
bool seqGate = false;
float seqFreq = 0.0f;
bool seqSlide = false;
bool seqAccent = false;
if (midiHeld)
{
@ -307,10 +529,21 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
const auto& s = sequencer.step (stepIdx);
seqSlide = s.slide;
seqAccent = s.accent;
// A step whose successor slides keeps its gate open until the next
// step so the glide into it is seamless; otherwise the gate closes
// after gateLen of the step.
const int nextIdx = (stepIdx + 1) % seqLen;
const bool slideIntoNext = sequencer.step (nextIdx).slide;
const float nextStart = (stepIdx + 1) * stepLen
+ (((stepIdx + 1) % 2) ? swing * stepLen : 0.0f);
// gate length: the step's note sustains for gateLen of the step
const float ppqWithinStep = ppqInCycle - stepStart;
seqGate = s.gate && (ppqWithinStep < stepLen * gateLen);
const float gateClose = slideIntoNext ? (nextStart - stepStart)
: stepLen * gateLen;
seqGate = s.gate && (ppqWithinStep < gateClose);
if (seqGate)
{
@ -332,6 +565,10 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
wantLegato = seqSlide;
}
// Accent tracks the current step (not just gate changes), so it holds for
// the whole step even when the note repeats without retriggering.
voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f);
if (wantGate != lastGateApplied)
{
if (wantGate)
@ -357,7 +594,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
}
void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
void MonostepAudioProcessor::setStepGate (int idx, bool gate)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
@ -368,7 +605,7 @@ void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
void MonostepAudioProcessor::setStepNote (int idx, int semitone)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
@ -379,7 +616,7 @@ void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepCents (int idx, float cents)
void MonostepAudioProcessor::setStepCents (int idx, float cents)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
@ -390,7 +627,7 @@ void MonoStepAudioProcessor::setStepCents (int idx, float cents)
onPatternChanged();
}
void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
void MonostepAudioProcessor::setStepSlide (int idx, bool slide)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
@ -401,7 +638,26 @@ void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
onPatternChanged();
}
juce::String MonoStepAudioProcessor::getStepNoteName (int idx) const
void MonostepAudioProcessor::setStepAccent (int idx, bool accent)
{
if (idx < 0 || idx >= monostep::numSteps)
return;
sequencer.step (idx).accent = accent;
if (onPatternChanged)
onPatternChanged();
}
void MonostepAudioProcessor::shiftPattern (int dir)
{
sequencer.shift (dir);
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;
@ -414,12 +670,13 @@ juce::String MonoStepAudioProcessor::getStepNoteName (int idx) const
return juce::MidiMessage::getMidiNoteName (note, true, true, 3);
}
void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
void MonostepAudioProcessor::storeSequence (juce::ValueTree& seq) const
{
juce::String gates;
juce::String notes;
juce::String cents;
juce::String slides;
juce::String accents;
for (int i = 0; i < monostep::numSteps; ++i)
{
@ -429,15 +686,17 @@ void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
notes += juce::String (s.semitone) + ",";
cents += juce::String (s.cents, 2) + ",";
slides += s.slide ? "1" : "0";
accents += s.accent ? "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);
seq.setProperty ("accents", accents, nullptr);
}
void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
void MonostepAudioProcessor::loadSequence (const juce::ValueTree& seq)
{
const auto splitFloats = [] (const juce::String& text)
{
@ -455,6 +714,7 @@ void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
const auto notes = splitFloats (seq.getProperty ("notes").toString());
const auto cents = splitFloats (seq.getProperty ("cents").toString());
const auto slides = seq.getProperty ("slides").toString();
const auto accents = seq.getProperty ("accents").toString();
for (int i = 0; i < monostep::numSteps; ++i)
{
@ -471,12 +731,15 @@ void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
if (i < slides.length())
s.slide = slides[i] == '1';
if (i < accents.length())
s.accent = accents[i] == '1';
}
}
void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
void MonostepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::ValueTree state = apvts.state.createCopy();
juce::ValueTree state = apvts.copyState();
juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
storeSequence (seq);
@ -484,7 +747,7 @@ void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
copyXmlToBinary (*xml, destData);
}
void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml (juce::AudioProcessor::getXmlFromBinary (data, sizeInBytes));
@ -501,10 +764,10 @@ void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBy
onPatternChanged();
}
juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
juce::AudioProcessorEditor* MonostepAudioProcessor::createEditor()
{
#ifndef MONOSTEP_HEADLESS
return new MonoStepAudioProcessorEditor (*this);
return new MonostepAudioProcessorEditor (*this);
#else
return nullptr;
#endif
@ -512,5 +775,5 @@ juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new MonoStepAudioProcessor();
return new MonostepAudioProcessor();
}

View file

@ -4,11 +4,11 @@
#include "dsp/SynthVoice.h"
#include "dsp/StepSequencer.h"
class MonoStepAudioProcessor final : public juce::AudioProcessor
class MonostepAudioProcessor final : public juce::AudioProcessor
{
public:
MonoStepAudioProcessor();
~MonoStepAudioProcessor() override = default;
MonostepAudioProcessor();
~MonostepAudioProcessor() override = default;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
@ -17,7 +17,7 @@ public:
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "MonoStep"; }
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; }
@ -43,6 +43,19 @@ public:
void setStepNote (int idx, int semitone);
void setStepCents (int idx, float cents);
void setStepSlide (int idx, bool slide);
void setStepAccent (int idx, bool accent);
void shiftPattern (int dir);
int getNumPresets() const;
const char* getPresetName (int index) const;
void applyPreset (int index);
void randomizePattern();
void randomizeParams();
void randomizeOsc();
void randomizeFilter();
void randomizeEnv();
void randomizeSeqSettings();
void randomizeFx();
juce::String getStepNoteName (int idx) const;
@ -52,6 +65,7 @@ private:
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
void updateVoiceParams();
void applyParamValue (const juce::String& id, float value);
float midiToFreq (float note) const;
void storeSequence (juce::ValueTree& seq) const;
void loadSequence (const juce::ValueTree& seq);
@ -61,8 +75,11 @@ private:
std::atomic<float>* osc1WaveParam = nullptr;
std::atomic<float>* osc2WaveParam = nullptr;
std::atomic<float>* osc1CoarseParam = nullptr;
std::atomic<float>* osc2CoarseParam = nullptr;
std::atomic<float>* detuneParam = nullptr;
std::atomic<float>* mixParam = nullptr;
std::atomic<float>* osc1PhaseParam = nullptr;
std::atomic<float>* cutoffParam = nullptr;
std::atomic<float>* resParam = nullptr;
std::atomic<float>* attackParam = nullptr;
@ -77,10 +94,14 @@ private:
std::atomic<float>* seqOctaveParam = nullptr;
std::atomic<float>* seqSwingParam = nullptr;
std::atomic<float>* seqGateLenParam = nullptr;
std::atomic<float>* driveParam = nullptr;
std::atomic<float>* ringModParam = nullptr;
std::atomic<float>* accentParam = nullptr;
monostep::StepSequencer sequencer;
monostep::SynthVoice voice;
juce::AudioBuffer<float> scratch;
juce::Random random;
double sampleRate = 44100.0;

View file

@ -15,7 +15,7 @@ namespace colours
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 accent2 (0xFFE8A33D);
inline const juce::Colour accentDim (0xFF7A5A26);
inline const juce::Colour activeCell (0xFF3A6EA5);
inline const juce::Colour selected (0xFFF0F4F8);

View file

@ -17,6 +17,7 @@ struct Step
int semitone = 0; // -12 .. +12
float cents = 0.0f; // -50 .. +50
bool slide = false;
bool accent = false;
};
class StepSequencer
@ -31,6 +32,20 @@ public:
s = Step{};
}
void shift (int dir)
{
if (dir == 0)
return;
std::array<Step, numSteps> rotated = steps;
for (int i = 0; i < numSteps; ++i)
{
const int from = (i - dir + numSteps) % numSteps;
steps[i] = rotated[from];
}
}
int stepsPerBeatFromRate (int rateIndex) const
{
// 0: 1/4, 1: 1/8, 2: 1/8T, 3: 1/16, 4: 1/32

View file

@ -13,8 +13,12 @@ public:
{
Waveform waveA = Waveform::saw;
Waveform waveB = Waveform::square;
float detuneRatio = 1.0f;
float mix = 0.5f;
float osc1Coarse = 0.0f;
float osc2Coarse = 0.0f;
float osc1Fine = 0.0f;
float osc2Fine = 0.0f;
float osc1Phase = 0.0f;
float osc2Phase = 0.0f;
float cutoff = 7000.0f;
float resonance = 0.15f;
float attack = 0.005f;
@ -23,11 +27,22 @@ public:
float release = 0.4f;
float glide = 0.12f;
float master = 0.9f;
float drive = 0.0f;
float ringMod = 0.0f;
float detuneRatio = 1.0f;
float mix = 0.5f;
};
void addPhase (float phaseOffset)
{
phaseA = frac (phaseA + phaseOffset);
phaseB = frac (phaseB + phaseOffset);
}
void prepare (double sr)
{
sampleRate = (float) sr;
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
reset();
}
@ -40,12 +55,16 @@ public:
env = 0.0f;
envStage = Stage::idle;
gate = false;
accentLevel = 1.0f;
accentSmooth = 1.0f;
}
void setParams (const Params& p) { params = p; }
bool isActive() const { return gate; }
void setAccentLevel (float level) { accentLevel = level; }
void noteOn (float freqHz, bool legato)
{
targetFreq = freqHz;
@ -153,6 +172,9 @@ private:
updateEnvelope();
updateGlide();
// Smooth the accent gain so it ramps in/out instead of clicking.
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef;
const float incA = currentFreq / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
@ -164,9 +186,24 @@ private:
float out = (1.0f - params.mix) * a + params.mix * b;
if (params.ringMod > 0.0f)
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
const float preFilter = out;
out = processFilter (out);
return out * env * params.master;
// Accented steps also get a touch of the unfiltered signal so they cut
// through the mix (presence), not just a volume bump.
if (accentSmooth > 1.0f)
out += (accentSmooth - 1.0f) * 0.2f * preFilter;
if (params.drive > 0.0f)
{
const float wet = std::tanh (out * (1.0f + params.drive * 9.0f));
out = (1.0f - params.drive) * out + params.drive * wet;
}
return out * env * params.master * accentSmooth;
}
float sampleRate = 44100.0f;
@ -184,6 +221,9 @@ private:
float env = 0.0f;
Stage envStage = Stage::idle;
bool gate = false;
float accentLevel = 1.0f;
float accentSmooth = 1.0f;
float accentSmoothCoef = 0.01f;
};
} // namespace monostep

View file

@ -2,6 +2,7 @@
#include <cmath>
#include <algorithm>
#include <random>
namespace monostep
{
@ -11,7 +12,11 @@ enum class Waveform : int
sine = 0,
triangle = 1,
saw = 2,
square = 3
square = 3,
pulse = 4,
noise = 5,
sub = 6,
pluck = 7
};
inline float frac (float x)
@ -63,6 +68,31 @@ inline float renderWave (Waveform w, float phase, float inc)
v -= polyBlep (frac (f + 0.5f), inc);
return v;
}
case Waveform::pulse:
{
const float f = frac (phase);
return (f < 0.3f) ? 1.0f : -1.0f;
}
case Waveform::sub:
{
const float f = frac (phase * 0.25f);
return 4.0f * std::min (f, 1.0f - f) - 1.0f;
}
case Waveform::noise:
{
thread_local static std::mt19937 rng { std::random_device {}() };
thread_local static std::uniform_real_distribution<float> dist (-1.0f, 1.0f);
return dist (rng);
}
case Waveform::pluck:
{
const float f = frac (phase);
return 2.0f * std::sin (f * 6.283185307179586f * 2.0f) * std::exp (-10.0f * f);
}
}
return 0.0f;

View file

@ -5,7 +5,7 @@
using namespace monostep;
SequencerMatrix::SequencerMatrix (MonoStepAudioProcessor& processorRef)
SequencerMatrix::SequencerMatrix (MonostepAudioProcessor& processorRef)
: processor (processorRef)
{
}
@ -22,13 +22,19 @@ int SequencerMatrix::semitoneForRow (int row) const
juce::Rectangle<int> SequencerMatrix::matrixBounds() const
{
const int w = getWidth();
const int h = getHeight();
if (w > labelW && h > headerH)
{
cellW = (float) (w - labelW) / (float) numSteps;
cellH = (float) (h - headerH) / (float) (numRows + 2);
}
const int gridW = juce::roundToInt (cellW * numSteps);
const int gridH = juce::roundToInt (cellH * (numRows + 1));
const int gridH = juce::roundToInt (cellH * (numRows + 2));
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 };
return { juce::roundToInt (labelW), juce::roundToInt (headerH), gridW, gridH };
}
void SequencerMatrix::setSelectedStep (int stepIdx)
@ -65,9 +71,16 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.fillRect (bounds.toFloat());
// selected column highlight
g.setColour (colours::selected.withAlpha (0.07f));
g.setColour (colours::selected.withAlpha (0.02f));
g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight());
// play position column highlight (drawn before cells so notes stay vivid)
if (playPos >= 0)
{
g.setColour (colours::accent.withAlpha (0.04f));
g.fillRect (ox + playPos * cellW, oy, cellW, (float) bounds.getHeight());
}
// grid lines
g.setColour (colours::grid);
@ -77,7 +90,7 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight());
}
for (int i = 0; i <= numRows + 1; ++i)
for (int i = 0; i <= numRows + 2; ++i)
{
const float y = oy + i * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
@ -107,14 +120,6 @@ void SequencerMatrix::paint (juce::Graphics& g)
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
@ -152,10 +157,10 @@ void SequencerMatrix::paint (juce::Graphics& g)
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(),
juce::ColourGradient grad (colours::accent,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
isSelected ? colours::accent.brighter (0.12f) : colours::accent.darker (0.35f),
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (grad);
@ -215,6 +220,39 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.fillPath (tri);
}
// --- accent row (bottom row) ----------------------------------------------
const float accentY = oy + (numRows + 1) * cellH;
g.setColour (colours::accentDim);
g.drawHorizontalLine (juce::roundToInt (accentY - 1.0f), ox, ox + (float) bounds.getWidth());
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("ACCENT",
juce::roundToInt (labelW * 0.1f), juce::roundToInt (accentY + 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).accent)
continue;
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, accentY + 1.5f,
cellW - 3.0f, cellH - 3.0f);
g.setColour (colours::accent2);
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 - 2.0f, ay - 2.5f, ax + 2.0f, ay - 2.5f, ax, ay + 2.5f);
g.fillPath (tri);
}
// --- dim columns beyond the pattern length ---------------------------------
if (seqLen < numSteps)
{
@ -222,21 +260,6 @@ void SequencerMatrix::paint (juce::Graphics& g)
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
@ -248,9 +271,9 @@ SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) co
const bool hit = bounds.contains (e.getPosition())
&& col >= 0 && col < numSteps
&& row >= 0 && row < numRows + 1;
&& row >= -1 && row < numRows + 2;
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows, row), hit };
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows + 1, row), hit };
}
void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
@ -260,11 +283,32 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
if (! cell.hit)
return;
if (cell.row == -1)
{
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
return;
}
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
if (cell.row == numRows + 1)
{
accentDragging = true;
accentLastStep = cell.step;
startedAccentActive = processor.getSequencer().step (cell.step).accent;
if (onAccentChanged)
onAccentChanged (cell.step, ! startedAccentActive);
repaint();
return;
}
if (cell.row == numRows)
{
slideDragging = true;
@ -299,13 +343,12 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
{
if (fineDragging)
{
const auto cell = cellAt (e);
if (! cell.hit)
if (! cell.hit || cell.row == -1)
return;
if (fineDragging)
{
// 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);
@ -325,8 +368,6 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
if (slideDragging)
{
const auto cell = cellAt (e);
if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
{
slideLastStep = cell.step;
@ -340,10 +381,26 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
return;
}
if (accentDragging)
{
if (cell.hit && cell.row == numRows + 1 && cell.step != accentLastStep)
{
accentLastStep = cell.step;
if (onAccentChanged)
onAccentChanged (cell.step, ! startedAccentActive);
repaint();
}
return;
}
if (! dragging)
return;
const auto cell = cellAt (e);
if (cell.row >= numRows)
return;
if (cell.hit && cell.row < numRows && cell.row != lastRow)
{
@ -352,6 +409,8 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
const int pitch = semitoneForRow (cell.row);
const auto& s = processor.getSequencer().step (cell.step);
if (startedActive)
{
if (onNoteChanged)
@ -384,6 +443,12 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
return;
}
if (accentDragging)
{
accentDragging = false;
return;
}
if (! dragging)
return;
@ -391,7 +456,7 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
const auto cell = cellAt (e);
if (! cell.hit || cell.step != dragStep || cell.row >= numRows)
if (! cell.hit || cell.step != dragStep || cell.row >= numRows || cell.row == -1)
return;
const int pitch = semitoneForRow (cell.row);

View file

@ -2,12 +2,12 @@
#include <JuceHeader.h>
class MonoStepAudioProcessor;
class MonostepAudioProcessor;
class SequencerMatrix final : public juce::Component
{
public:
explicit SequencerMatrix (MonoStepAudioProcessor& processor);
explicit SequencerMatrix (MonostepAudioProcessor& processor);
~SequencerMatrix() override = default;
std::function<void (int stepIdx)> onStepSelected;
@ -15,6 +15,7 @@ public:
std::function<void (int stepIdx, int semitone)> onNoteChanged;
std::function<void (int stepIdx, float cents)> onFineChanged;
std::function<void (int stepIdx, bool slide)> onSlideChanged;
std::function<void (int stepIdx, bool accent)> onAccentChanged;
void setSelectedStep (int stepIdx);
int getSelectedStep() const { return selectedStep; }
@ -39,14 +40,15 @@ private:
int rowForSemitone (int semitone) const;
int semitoneForRow (int row) const;
MonoStepAudioProcessor& processor;
MonostepAudioProcessor& processor;
float cellW = 24.0f;
float cellH = 17.0f;
float labelW = 40.0f;
float headerH = 24.0f;
float headerPad = 10.0f;
mutable float cellW = 24.0f;
mutable float cellH = 17.0f;
int selectedStep = 0;
int playPos = -1;
@ -64,4 +66,8 @@ private:
bool slideDragging = false;
bool startedSlideActive = false;
int slideLastStep = -1;
bool accentDragging = false;
bool startedAccentActive = false;
int accentLastStep = -1;
};

View file

@ -27,7 +27,8 @@ static bool patternsEqual (const monostep::StepSequencer& a, const monostep::Ste
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)
|| std::fabs (sa.cents - sb.cents) > 0.001f
|| sa.slide != sb.slide || sa.accent != sb.accent)
return false;
}
@ -63,7 +64,7 @@ static int testVst3Binary (const juce::File& pluginFile)
// Part B: drive the actual processor source and render audio.
static int testProcessorRendering()
{
MonoStepAudioProcessor processor;
MonostepAudioProcessor processor;
processor.prepareToPlay (44100.0, 512);
const int totalSamples = (int) (44100.0 * 8.0);
@ -96,7 +97,7 @@ static int testProcessorRendering()
}
// Without a MIDI note the sequencer must stay silent.
MonoStepAudioProcessor idleProcessor;
MonostepAudioProcessor idleProcessor;
idleProcessor.prepareToPlay (44100.0, 512);
double idleSum = 0.0;
@ -129,6 +130,7 @@ static int testProcessorRendering()
processor.setStepNote (0, 7);
processor.setStepCents (0, 42.0f);
processor.setStepSlide (1, true);
processor.setStepAccent (0, true);
if (std::fabs (processor.getSequencer().step (0).cents - 42.0f) > 0.001f)
{
@ -136,12 +138,97 @@ static int testProcessorRendering()
return 1;
}
// accent DSP: a uniform continuous pattern (all same note, gate held open)
// so no per-note retrigger masks whether accent follows the step.
*processor.getAPVTS().getRawParameterValue ("accent") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("seqGateLen") = 1.0f;
for (int i = 0; i < monostep::numSteps; ++i)
{
processor.setStepGate (i, true);
processor.setStepNote (i, 0);
processor.setStepSlide (i, false);
}
const auto renderAccent = [&] (const std::function<bool (int)>& accented) -> double
{
for (int i = 0; i < monostep::numSteps; ++i)
processor.setStepAccent (i, accented (i));
processor.reset();
processor.prepareToPlay (44100.0, 512);
juce::AudioBuffer<float> accBlock (2, blockSize);
juce::MidiBuffer accMidi;
double sum = 0.0;
for (int b = 0; b < numBlocks; ++b)
{
accBlock.clear();
accMidi.clear();
if (b == 0)
accMidi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
processor.processBlock (accBlock, accMidi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
sum += (double) accBlock.getSample (c, i) * accBlock.getSample (c, i);
}
return std::sqrt (sum / (double) (numBlocks * blockSize * 2));
};
const double accRms = renderAccent ([] (int) { return true; });
const double noAccRms = renderAccent ([] (int) { return false; });
const double altRms = renderAccent ([] (int i) { return (i % 2) == 0; });
if (accRms < noAccRms * 1.05f)
{
std::cout << "FAILED: accent does not boost the step (acc=" << accRms
<< " noacc=" << noAccRms << ")\n";
return 1;
}
if (altRms < noAccRms * 1.05f || altRms > accRms * 0.9f)
{
std::cout << "FAILED: accent does not track individual steps (all=" << accRms
<< " alt=" << altRms << " noacc=" << noAccRms << ")\n";
return 1;
}
std::cout << "Accent boosts step volume OK (all=" << accRms
<< " alternating=" << altRms << " none=" << noAccRms << ")\n";
// pattern shift rotates the steps
processor.setStepAccent (0, true);
processor.setStepNote (0, 7);
processor.setStepNote (1, 3);
processor.shiftPattern (1);
if (processor.getSequencer().step (1).semitone != 7)
{
std::cout << "FAILED: shiftPattern did not rotate steps\n";
return 1;
}
processor.shiftPattern (-1);
if (processor.getSequencer().step (0).semitone != 7 || ! processor.getSequencer().step (0).accent)
{
std::cout << "FAILED: shiftPattern did not rotate back\n";
return 1;
}
std::cout << "Pattern shift rotates steps OK\n";
// state round trip through a fresh instance
juce::MemoryBlock state;
processor.getStateInformation (state);
std::cout << "State bytes: " << (int) state.getSize() << "\n";
MonoStepAudioProcessor processor2;
MonostepAudioProcessor processor2;
processor2.prepareToPlay (44100.0, 512);
processor2.setStateInformation (state.getData(), (int) state.getSize());
@ -152,7 +239,8 @@ static int testProcessorRendering()
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";
<< " slide=" << a.slide << "/" << b.slide
<< " accent=" << a.accent << "/" << b.accent << "\n";
}
if (! patternsEqual (processor.getSequencer(), processor2.getSequencer()))
@ -218,7 +306,7 @@ int main (int argc, char** argv)
{
if (argc < 2)
{
std::cout << "Usage: MonoStepTestHost <plugin.vst3> [out.wav]\n";
std::cout << "Usage: MonostepTestHost <plugin.vst3> [out.wav]\n";
return 1;
}