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

View file

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

View file

@ -1,4 +1,4 @@
# MonoStep # Monostep
A monophonic 2-oscillator step-sequencer synthesizer implemented as a VST3 plugin with JUCE. 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) - 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 - 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 - 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 - Footer status bar showing the git commit and build date
- Pattern stored in the plugin state (save/restore with the DAW) - 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 cmake -S . -B build -DJUCE_DIR=/path/to/juce
# 3. Build the plugin: # 3. Build the plugin:
cmake --build build --target MonoStep_VST3 cmake --build build --target Monostep_VST3
``` ```
The VST3 plugin is written to: The VST3 plugin is written to:
``` ```
build/MonoStep_artefacts/Release/VST3/MonoStep.vst3 build/Monostep_artefacts/Release/VST3/Monostep.vst3
``` ```
### Quick start with make ### Quick start with make
@ -58,11 +58,11 @@ make clean # remove the build directory
### Installing (manual copy) ### 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: bundle into your DAW's VST3 directory:
```sh ```sh
cp -R build/MonoStep_artefacts/Release/VST3/MonoStep.vst3 \ cp -R build/Monostep_artefacts/Release/VST3/Monostep.vst3 \
~/Library/Audio/Plug-Ins/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 - 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 with `mkdir -p` if it does not exist. A system-wide copy would go to
`/Library/Audio/Plug-Ins/VST3/` but requires `sudo`. `/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. inside the target directory) so stale files do not remain.
- Your DAW may cache the plugin list — quit and relaunch it (or rescan plugins) - 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 ## Running the tests
Build the headless host test and run it against the built plugin: Build the headless host test and run it against the built plugin:
```sh ```sh
cmake --build build --target MonoStepTestHost cmake --build build --target MonostepTestHost
build/MonoStepTestHost_artefacts/Release/MonoStepTestHost \ build/MonostepTestHost_artefacts/Release/MonostepTestHost \
build/MonoStep_artefacts/Release/VST3/MonoStep.vst3 build/Monostep_artefacts/Release/VST3/Monostep.vst3
``` ```
The host scans the VST3, renders a buffer, and verifies that the pattern The host scans the VST3, renders a buffer, and verifies that the pattern

View file

@ -6,6 +6,19 @@
using namespace monostep; 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() EditorLookAndFeel::EditorLookAndFeel()
{ {
@ -33,6 +46,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
g.setColour (colours::gridLight); g.setColour (colours::gridLight);
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f); 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) if (end - start < 6.0f)
{ {
juce::Path valueArc; 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, radius * 0.85f, radius * 0.85f,
0.0f, start, angle, true); 0.0f, start, angle, true);
g.setColour (colours::accent); g.setGradientFill (orangeGradient (bounds));
g.strokePath (valueArc, juce::PathStrokeType (2.5f)); g.strokePath (valueArc, juce::PathStrokeType (2.5f));
} }
// JUCE measures rotary angles clockwise from 12 o'clock; the pointer is // The pointer reaches out to the value arc so it touches the arc.
// drawn with the same convention so it matches the arc and mouse dragging. const float tickLen = radius * 0.85f + 1.0f;
const float tickLen = radius * 0.62f; g.setGradientFill (orangeGradient (bounds));
g.setColour (colours::accent);
g.drawLine (centre.getX(), centre.getY(), g.drawLine (centre.getX(), centre.getY(),
centre.getX() + tickLen * std::sin (angle), centre.getX() + tickLen * std::sin (angle),
centre.getY() - tickLen * std::cos (angle), centre.getY() - tickLen * std::cos (angle),
2.5f); 2.5f);
g.setColour (colours::accent);
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f); 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(); 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.fillRoundedRectangle (bounds, 4.0f);
g.setFont (font (11.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); const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState()) if (button.getToggleState())
g.setColour (colours::accent); g.setGradientFill (orangeGradient (bounds));
else else
g.setColour (highlighted ? colours::gridLight : colours::grid); 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); 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) Knob::Knob (const juce::String& title)
{ {
@ -124,10 +229,14 @@ Knob::Knob (const juce::String& title)
void Knob::resized() void Knob::resized()
{ {
const int w = getWidth(); const int w = getWidth();
const int h = getHeight();
titleLabel.setBounds (0, 0, w, 14); 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() void Knob::updateValueLabel()
@ -234,7 +343,7 @@ void ChoiceBar::resized()
} }
//============================================================================== //==============================================================================
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef) StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
: processor (processorRef), : processor (processorRef),
fineKnob ("Fine") fineKnob ("Fine")
{ {
@ -284,6 +393,70 @@ StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
clearButton.onClick = [this] { clearPattern(); }; clearButton.onClick = [this] { clearPattern(); };
addAndMakeVisible (clearButton); 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); setSelectedStep (0);
} }
@ -328,6 +501,7 @@ void StepPanel::clearPattern()
processor.setStepNote (i, 0); processor.setStepNote (i, 0);
processor.setStepCents (i, 0.0f); processor.setStepCents (i, 0.0f);
processor.setStepSlide (i, false); processor.setStepSlide (i, false);
processor.setStepAccent (i, false);
} }
refresh(); refresh();
@ -344,18 +518,37 @@ void StepPanel::resized()
const int knobX = (w - 74) / 2; const int knobX = (w - 74) / 2;
const int knobY = 68; 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); prevButton.setBounds (knobX - 32, btnY, 24, 24);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24); nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
hintLabel.setBounds (8, knobY + 82, w - 16, 14); hintLabel.setBounds (8, btnY + 30, w - 16, 14);
clearButton.setBounds (10, knobY + 106, w - 20, 26);
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), : AudioProcessorEditor (processor),
processorRef (processor), processorRef (processor),
content (*this), content (*this),
@ -365,8 +558,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
rootKnob ("Root"), rootKnob ("Root"),
gateKnob ("Gate"), gateKnob ("Gate"),
masterKnob ("Master"), masterKnob ("Master"),
coarseKnob ("Coarse"),
coarseKnob2 ("Coarse"),
detuneKnob ("Detune"), detuneKnob ("Detune"),
mixKnob ("Mix"), mixKnob ("Mix"),
fineKnob1 ("Fine"),
fineKnob2 ("Fine"),
cutoffKnob ("Cutoff"), cutoffKnob ("Cutoff"),
resKnob ("Res"), resKnob ("Res"),
glideKnob ("Glide"), glideKnob ("Glide"),
@ -374,6 +571,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
decayKnob ("Decay"), decayKnob ("Decay"),
sustainKnob ("Sustain"), sustainKnob ("Sustain"),
releaseKnob ("Release"), releaseKnob ("Release"),
driveKnob ("Drive"),
ringKnob ("Ring"),
accentKnob ("Accent"),
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }), rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }), octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}), 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 (rootKnob, "seqRoot", [] (float v) { return juce::String ((int) v); });
addKnob (gateKnob, "seqGateLen", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); 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 (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 (detuneKnob, "detune", [] (float v) { return juce::String (juce::roundToInt (v)) + " ct"; });
addKnob (mixKnob, "mix", [] (float v) { return juce::String (v, 2); }); 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 (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 (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); }); addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; }); 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 (matrix);
content.addAndMakeVisible (stepPanel); content.addAndMakeVisible (stepPanel);
@ -466,6 +670,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
stepPanel.refresh(); stepPanel.refresh();
}; };
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
{
processorRef.setStepAccent (stepIdx, accent);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); }; processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30); startTimerHz (30);
@ -473,14 +683,14 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
refreshEditor(); refreshEditor();
} }
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor() MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
{ {
stopTimer(); stopTimer();
processorRef.onPatternChanged = nullptr; processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr); setLookAndFeel (nullptr);
} }
void MonoStepAudioProcessorEditor::timerCallback() void MonostepAudioProcessorEditor::timerCallback()
{ {
const int step = processorRef.getCurrentStep(); const int step = processorRef.getCurrentStep();
@ -491,27 +701,27 @@ void MonoStepAudioProcessorEditor::timerCallback()
} }
} }
void MonoStepAudioProcessorEditor::refreshEditor() void MonostepAudioProcessorEditor::refreshEditor()
{ {
matrix.setSelectedStep (matrix.getSelectedStep()); matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh(); stepPanel.refresh();
matrix.repaint(); matrix.repaint();
} }
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g) void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
{ {
g.fillAll (colours::bg); g.fillAll (colours::bg);
} }
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g) void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
{ {
g.setFont (font (22.0f)); g.setFont (font (22.0f));
g.setColour (colours::text); 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.setFont (font (9.0f));
g.setColour (colours::textDim); 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) 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); 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; zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * 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 targetW = juce::roundToInt (baseWidth * zoomFactor);
const int targetH = juce::roundToInt (baseHeight * zoomFactor); const int targetH = juce::roundToInt (baseHeight * zoomFactor);
@ -563,18 +773,19 @@ void MonoStepAudioProcessorEditor::resized()
const int w = baseWidth; const int w = baseWidth;
const int h = baseHeight; const int h = baseHeight;
const int topBarH = 58; const int topBarH = 78;
const int bottomBarH = 96; const int bottomBarH = 96;
const int pad = 12; const int pad = 12;
const int topBarY = (topBarH - 26) / 2;
lengthKnob.setBounds (128, 2, 54, 56); lengthKnob.setBounds (128, 2, 54, 74);
rateBar.setBounds (192, 18, 210, 26); rateBar.setBounds (192, topBarY, 210, 26);
swingKnob.setBounds (422, 2, 54, 56); swingKnob.setBounds (422, 2, 54, 74);
rootKnob.setBounds (482, 2, 54, 56); rootKnob.setBounds (482, 2, 54, 74);
octaveBar.setBounds (548, 18, 170, 26); octaveBar.setBounds (548, topBarY, 170, 26);
zoomBar.setBounds (w - pad - 54 - 8 - 236, 18, 236, 26); zoomBar.setBounds (w - pad - 54 - 8 - 236, topBarY, 236, 26);
masterKnob.setBounds (w - pad - 54, 2, 54, 56); masterKnob.setBounds (w - pad - 54, 2, 54, 74);
const int panelW = 200; const int panelW = 200;
const int midTop = topBarH; const int midTop = topBarH;
@ -592,7 +803,7 @@ void MonoStepAudioProcessorEditor::resized()
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16; wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56; const int knobW = 56;
const int knobGap = 72; const int knobGap = 60;
const int knobY = bottomY + 2; const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob) const auto placeKnob = [&] (Knob& knob)
@ -601,6 +812,7 @@ void MonoStepAudioProcessorEditor::resized()
x += knobGap; x += knobGap;
}; };
placeKnob (coarseKnob);
placeKnob (detuneKnob); placeKnob (detuneKnob);
placeKnob (mixKnob); placeKnob (mixKnob);
placeKnob (cutoffKnob); placeKnob (cutoffKnob);
@ -610,4 +822,7 @@ void MonoStepAudioProcessorEditor::resized()
placeKnob (decayKnob); placeKnob (decayKnob);
placeKnob (sustainKnob); placeKnob (sustainKnob);
placeKnob (releaseKnob); placeKnob (releaseKnob);
placeKnob (driveKnob);
placeKnob (ringKnob);
placeKnob (accentKnob);
} }

View file

@ -3,7 +3,7 @@
#include <JuceHeader.h> #include <JuceHeader.h>
#include "ui/SequencerMatrix.h" #include "ui/SequencerMatrix.h"
class MonoStepAudioProcessor; class MonostepAudioProcessor;
class EditorLookAndFeel final : public juce::LookAndFeel_V4 class EditorLookAndFeel final : public juce::LookAndFeel_V4
{ {
@ -23,6 +23,17 @@ public:
void drawButtonText (juce::Graphics&, juce::TextButton&, void drawButtonText (juce::Graphics&, juce::TextButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override; 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 class Knob final : public juce::Component
@ -93,7 +104,7 @@ private:
class StepPanel final : public juce::Component class StepPanel final : public juce::Component
{ {
public: public:
explicit StepPanel (MonoStepAudioProcessor& processor); explicit StepPanel (MonostepAudioProcessor& processor);
void setSelectedStep (int stepIdx); void setSelectedStep (int stepIdx);
void refresh(); void refresh();
@ -105,7 +116,7 @@ public:
private: private:
void clearPattern(); void clearPattern();
MonoStepAudioProcessor& processor; MonostepAudioProcessor& processor;
int selectedStep = 0; int selectedStep = 0;
bool updating = false; bool updating = false;
@ -118,14 +129,34 @@ private:
juce::TextButton nextButton; juce::TextButton nextButton;
juce::Label hintLabel; juce::Label hintLabel;
juce::TextButton clearButton; 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 juce::Timer
{ {
public: public:
explicit MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor); explicit MonostepAudioProcessorEditor (MonostepAudioProcessor& processor);
~MonoStepAudioProcessorEditor() override; ~MonostepAudioProcessorEditor() override;
void paint (juce::Graphics&) override; void paint (juce::Graphics&) override;
void resized() override; void resized() override;
@ -138,15 +169,15 @@ private:
class ContentComponent final : public juce::Component class ContentComponent final : public juce::Component
{ {
public: public:
explicit ContentComponent (MonoStepAudioProcessorEditor& owner) : ownerRef (owner) {} explicit ContentComponent (MonostepAudioProcessorEditor& owner) : ownerRef (owner) {}
void paint (juce::Graphics& g) override { ownerRef.paintContent (g); } void paint (juce::Graphics& g) override { ownerRef.paintContent (g); }
private: private:
MonoStepAudioProcessorEditor& ownerRef; MonostepAudioProcessorEditor& ownerRef;
}; };
MonoStepAudioProcessor& processorRef; MonostepAudioProcessor& processorRef;
ContentComponent content; ContentComponent content;
@ -165,8 +196,12 @@ private:
Knob rootKnob; Knob rootKnob;
Knob gateKnob; Knob gateKnob;
Knob masterKnob; Knob masterKnob;
Knob coarseKnob;
Knob coarseKnob2;
Knob detuneKnob; Knob detuneKnob;
Knob mixKnob; Knob mixKnob;
Knob fineKnob1;
Knob fineKnob2;
Knob cutoffKnob; Knob cutoffKnob;
Knob resKnob; Knob resKnob;
Knob glideKnob; Knob glideKnob;
@ -174,6 +209,9 @@ private:
Knob decayKnob; Knob decayKnob;
Knob sustainKnob; Knob sustainKnob;
Knob releaseKnob; Knob releaseKnob;
Knob driveKnob;
Knob ringKnob;
Knob accentKnob;
ChoiceBar rateBar; ChoiceBar rateBar;
ChoiceBar octaveBar; ChoiceBar octaveBar;

View file

@ -8,30 +8,35 @@ static const char* paramId (int index)
{ {
static const char* ids[] = static const char* ids[] =
{ {
"osc1Wave", "osc2Wave", "detune", "mix", "osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master", "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]; return ids[index];
} }
enum ParamIndex enum ParamIndex
{ {
pOsc1Wave, pOsc2Wave, pDetune, pMix, pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster, pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen, pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
pDrive, pRingMod, pAccent, pFine1,
numParams numParams
}; };
MonoStepAudioProcessor::MonoStepAudioProcessor() MonostepAudioProcessor::MonostepAudioProcessor()
: AudioProcessor (BusesProperties() : AudioProcessor (BusesProperties()
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)) .withOutput ("Output", juce::AudioChannelSet::stereo(), true))
, apvts (*this, nullptr, "PARAMS", createParameterLayout()) , apvts (*this, nullptr, "PARAMS", createParameterLayout())
{ {
osc1WaveParam = apvts.getRawParameterValue (paramId (pOsc1Wave)); osc1WaveParam = apvts.getRawParameterValue (paramId (pOsc1Wave));
osc2WaveParam = apvts.getRawParameterValue (paramId (pOsc2Wave)); osc2WaveParam = apvts.getRawParameterValue (paramId (pOsc2Wave));
osc1CoarseParam = apvts.getRawParameterValue (paramId (pOsc1Coarse));
osc2CoarseParam = apvts.getRawParameterValue (paramId (pOsc2Coarse));
detuneParam = apvts.getRawParameterValue (paramId (pDetune)); detuneParam = apvts.getRawParameterValue (paramId (pDetune));
mixParam = apvts.getRawParameterValue (paramId (pMix)); mixParam = apvts.getRawParameterValue (paramId (pMix));
osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1));
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff)); cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
resParam = apvts.getRawParameterValue (paramId (pRes)); resParam = apvts.getRawParameterValue (paramId (pRes));
attackParam = apvts.getRawParameterValue (paramId (pAttack)); attackParam = apvts.getRawParameterValue (paramId (pAttack));
@ -46,16 +51,22 @@ MonoStepAudioProcessor::MonoStepAudioProcessor()
seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave)); seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave));
seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing)); seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing));
seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen)); seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen));
driveParam = apvts.getRawParameterValue (paramId (pDrive));
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
accentParam = apvts.getRawParameterValue (paramId (pAccent));
setDefaultPattern(); setDefaultPattern();
} }
juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::createParameterLayout() juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::createParameterLayout()
{ {
juce::AudioProcessorValueTreeState::ParameterLayout layout; 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 (pOsc1Wave), "Osc 1 Wave", 0, 7, 2));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 3, 3)); 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), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDetune),
"Osc 2 Detune", juce::NormalisableRange<float> (-100.0f, 100.0f), 0.0f, "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), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMix),
"Osc Mix", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f)); "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), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pCutoff),
"Filter Cutoff", juce::NormalisableRange<float> (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f, "Filter Cutoff", juce::NormalisableRange<float> (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f,
juce::AudioParameterFloatAttributes().withLabel ("Hz"))); juce::AudioParameterFloatAttributes().withLabel ("Hz")));
@ -109,10 +124,19 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f)); "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; return layout;
} }
void MonoStepAudioProcessor::setDefaultPattern() void MonostepAudioProcessor::setDefaultPattern()
{ {
struct Def { int step; bool gate; int semitone; bool slide; }; struct Def { int step; bool gate; int semitone; bool slide; };
const Def defs[] = const Def defs[] =
@ -136,9 +160,199 @@ void MonoStepAudioProcessor::setDefaultPattern()
sequencer.step (d.step).semitone = d.semitone; sequencer.step (d.step).semitone = d.semitone;
sequencer.step (d.step).slide = d.slide; 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; sampleRate = sr;
voice.prepare (sr); voice.prepare (sr);
@ -151,17 +365,18 @@ void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
lastHostPpq = -1.0; lastHostPpq = -1.0;
} }
void MonoStepAudioProcessor::releaseResources() void MonostepAudioProcessor::releaseResources()
{ {
voice.reset(); voice.reset();
} }
void MonoStepAudioProcessor::updateVoiceParams() void MonostepAudioProcessor::updateVoiceParams()
{ {
monostep::SynthVoice::Params p; monostep::SynthVoice::Params p;
p.waveA = (monostep::Waveform) (int) osc1WaveParam->load(); p.waveA = (monostep::Waveform) (int) osc1WaveParam->load();
p.waveB = (monostep::Waveform) (int) osc2WaveParam->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.mix = mixParam->load();
p.cutoff = cutoffParam->load(); p.cutoff = cutoffParam->load();
p.resonance = resParam->load(); p.resonance = resParam->load();
@ -171,15 +386,17 @@ void MonoStepAudioProcessor::updateVoiceParams()
p.release = releaseParam->load(); p.release = releaseParam->load();
p.glide = glideParam->load(); p.glide = glideParam->load();
p.master = masterParam->load(); p.master = masterParam->load();
p.drive = driveParam->load();
p.ringMod = ringModParam->load();
voice.setParams (p); 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); 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; juce::ScopedNoDenormals noDenormals;
@ -265,8 +482,12 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
} }
else else
{ {
if (lastGateApplied)
voice.noteOff();
lastStep = -1; lastStep = -1;
lastGateApplied = false; lastGateApplied = false;
lastFreqApplied = -1.0f;
playStep.store (-1); playStep.store (-1);
} }
@ -277,6 +498,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
bool seqGate = false; bool seqGate = false;
float seqFreq = 0.0f; float seqFreq = 0.0f;
bool seqSlide = false; bool seqSlide = false;
bool seqAccent = false;
if (midiHeld) if (midiHeld)
{ {
@ -307,10 +529,21 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
const auto& s = sequencer.step (stepIdx); const auto& s = sequencer.step (stepIdx);
seqSlide = s.slide; 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; 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) if (seqGate)
{ {
@ -332,6 +565,10 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
wantLegato = seqSlide; 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 != lastGateApplied)
{ {
if (wantGate) if (wantGate)
@ -357,7 +594,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples); 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) if (idx < 0 || idx >= monostep::numSteps)
return; return;
@ -368,7 +605,7 @@ void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
onPatternChanged(); onPatternChanged();
} }
void MonoStepAudioProcessor::setStepNote (int idx, int semitone) void MonostepAudioProcessor::setStepNote (int idx, int semitone)
{ {
if (idx < 0 || idx >= monostep::numSteps) if (idx < 0 || idx >= monostep::numSteps)
return; return;
@ -379,7 +616,7 @@ void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
onPatternChanged(); onPatternChanged();
} }
void MonoStepAudioProcessor::setStepCents (int idx, float cents) void MonostepAudioProcessor::setStepCents (int idx, float cents)
{ {
if (idx < 0 || idx >= monostep::numSteps) if (idx < 0 || idx >= monostep::numSteps)
return; return;
@ -390,7 +627,7 @@ void MonoStepAudioProcessor::setStepCents (int idx, float cents)
onPatternChanged(); onPatternChanged();
} }
void MonoStepAudioProcessor::setStepSlide (int idx, bool slide) void MonostepAudioProcessor::setStepSlide (int idx, bool slide)
{ {
if (idx < 0 || idx >= monostep::numSteps) if (idx < 0 || idx >= monostep::numSteps)
return; return;
@ -401,7 +638,26 @@ void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
onPatternChanged(); 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 root = juce::roundToInt (seqRootParam->load());
const int octave = juce::roundToInt (seqOctaveParam->load()) - 2; 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); 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 gates;
juce::String notes; juce::String notes;
juce::String cents; juce::String cents;
juce::String slides; juce::String slides;
juce::String accents;
for (int i = 0; i < monostep::numSteps; ++i) for (int i = 0; i < monostep::numSteps; ++i)
{ {
@ -429,15 +686,17 @@ void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
notes += juce::String (s.semitone) + ","; notes += juce::String (s.semitone) + ",";
cents += juce::String (s.cents, 2) + ","; cents += juce::String (s.cents, 2) + ",";
slides += s.slide ? "1" : "0"; slides += s.slide ? "1" : "0";
accents += s.accent ? "1" : "0";
} }
seq.setProperty ("gates", gates, nullptr); seq.setProperty ("gates", gates, nullptr);
seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr); seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr);
seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr); seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr);
seq.setProperty ("slides", slides, 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) 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 notes = splitFloats (seq.getProperty ("notes").toString());
const auto cents = splitFloats (seq.getProperty ("cents").toString()); const auto cents = splitFloats (seq.getProperty ("cents").toString());
const auto slides = seq.getProperty ("slides").toString(); const auto slides = seq.getProperty ("slides").toString();
const auto accents = seq.getProperty ("accents").toString();
for (int i = 0; i < monostep::numSteps; ++i) for (int i = 0; i < monostep::numSteps; ++i)
{ {
@ -471,12 +731,15 @@ void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
if (i < slides.length()) if (i < slides.length())
s.slide = slides[i] == '1'; 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); juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
storeSequence (seq); storeSequence (seq);
@ -484,7 +747,7 @@ void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
copyXmlToBinary (*xml, 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)); std::unique_ptr<juce::XmlElement> xml (juce::AudioProcessor::getXmlFromBinary (data, sizeInBytes));
@ -501,10 +764,10 @@ void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBy
onPatternChanged(); onPatternChanged();
} }
juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor() juce::AudioProcessorEditor* MonostepAudioProcessor::createEditor()
{ {
#ifndef MONOSTEP_HEADLESS #ifndef MONOSTEP_HEADLESS
return new MonoStepAudioProcessorEditor (*this); return new MonostepAudioProcessorEditor (*this);
#else #else
return nullptr; return nullptr;
#endif #endif
@ -512,5 +775,5 @@ juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{ {
return new MonoStepAudioProcessor(); return new MonostepAudioProcessor();
} }

View file

@ -4,11 +4,11 @@
#include "dsp/SynthVoice.h" #include "dsp/SynthVoice.h"
#include "dsp/StepSequencer.h" #include "dsp/StepSequencer.h"
class MonoStepAudioProcessor final : public juce::AudioProcessor class MonostepAudioProcessor final : public juce::AudioProcessor
{ {
public: public:
MonoStepAudioProcessor(); MonostepAudioProcessor();
~MonoStepAudioProcessor() override = default; ~MonostepAudioProcessor() override = default;
void prepareToPlay (double sampleRate, int samplesPerBlock) override; void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override; void releaseResources() override;
@ -17,7 +17,7 @@ public:
juce::AudioProcessorEditor* createEditor() override; juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; } 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 acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; } bool producesMidi() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; } double getTailLengthSeconds() const override { return 0.0; }
@ -43,6 +43,19 @@ public:
void setStepNote (int idx, int semitone); void setStepNote (int idx, int semitone);
void setStepCents (int idx, float cents); void setStepCents (int idx, float cents);
void setStepSlide (int idx, bool slide); 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; juce::String getStepNoteName (int idx) const;
@ -52,6 +65,7 @@ private:
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
void updateVoiceParams(); void updateVoiceParams();
void applyParamValue (const juce::String& id, float value);
float midiToFreq (float note) const; float midiToFreq (float note) const;
void storeSequence (juce::ValueTree& seq) const; void storeSequence (juce::ValueTree& seq) const;
void loadSequence (const juce::ValueTree& seq); void loadSequence (const juce::ValueTree& seq);
@ -61,8 +75,11 @@ private:
std::atomic<float>* osc1WaveParam = nullptr; std::atomic<float>* osc1WaveParam = nullptr;
std::atomic<float>* osc2WaveParam = nullptr; std::atomic<float>* osc2WaveParam = nullptr;
std::atomic<float>* osc1CoarseParam = nullptr;
std::atomic<float>* osc2CoarseParam = nullptr;
std::atomic<float>* detuneParam = nullptr; std::atomic<float>* detuneParam = nullptr;
std::atomic<float>* mixParam = nullptr; std::atomic<float>* mixParam = nullptr;
std::atomic<float>* osc1PhaseParam = nullptr;
std::atomic<float>* cutoffParam = nullptr; std::atomic<float>* cutoffParam = nullptr;
std::atomic<float>* resParam = nullptr; std::atomic<float>* resParam = nullptr;
std::atomic<float>* attackParam = nullptr; std::atomic<float>* attackParam = nullptr;
@ -75,12 +92,16 @@ private:
std::atomic<float>* seqRateParam = nullptr; std::atomic<float>* seqRateParam = nullptr;
std::atomic<float>* seqRootParam = nullptr; std::atomic<float>* seqRootParam = nullptr;
std::atomic<float>* seqOctaveParam = nullptr; std::atomic<float>* seqOctaveParam = nullptr;
std::atomic<float>* seqSwingParam = nullptr; std::atomic<float>* seqSwingParam = nullptr;
std::atomic<float>* seqGateLenParam = 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::StepSequencer sequencer;
monostep::SynthVoice voice; monostep::SynthVoice voice;
juce::AudioBuffer<float> scratch; juce::AudioBuffer<float> scratch;
juce::Random random;
double sampleRate = 44100.0; double sampleRate = 44100.0;

View file

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

View file

@ -17,6 +17,7 @@ struct Step
int semitone = 0; // -12 .. +12 int semitone = 0; // -12 .. +12
float cents = 0.0f; // -50 .. +50 float cents = 0.0f; // -50 .. +50
bool slide = false; bool slide = false;
bool accent = false;
}; };
class StepSequencer class StepSequencer
@ -31,6 +32,20 @@ public:
s = Step{}; 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 int stepsPerBeatFromRate (int rateIndex) const
{ {
// 0: 1/4, 1: 1/8, 2: 1/8T, 3: 1/16, 4: 1/32 // 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 waveA = Waveform::saw;
Waveform waveB = Waveform::square; Waveform waveB = Waveform::square;
float detuneRatio = 1.0f; float osc1Coarse = 0.0f;
float mix = 0.5f; 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 cutoff = 7000.0f;
float resonance = 0.15f; float resonance = 0.15f;
float attack = 0.005f; float attack = 0.005f;
@ -23,11 +27,22 @@ public:
float release = 0.4f; float release = 0.4f;
float glide = 0.12f; float glide = 0.12f;
float master = 0.9f; 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) void prepare (double sr)
{ {
sampleRate = (float) sr; sampleRate = (float) sr;
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
reset(); reset();
} }
@ -40,12 +55,16 @@ public:
env = 0.0f; env = 0.0f;
envStage = Stage::idle; envStage = Stage::idle;
gate = false; gate = false;
accentLevel = 1.0f;
accentSmooth = 1.0f;
} }
void setParams (const Params& p) { params = p; } void setParams (const Params& p) { params = p; }
bool isActive() const { return gate; } bool isActive() const { return gate; }
void setAccentLevel (float level) { accentLevel = level; }
void noteOn (float freqHz, bool legato) void noteOn (float freqHz, bool legato)
{ {
targetFreq = freqHz; targetFreq = freqHz;
@ -153,6 +172,9 @@ private:
updateEnvelope(); updateEnvelope();
updateGlide(); updateGlide();
// Smooth the accent gain so it ramps in/out instead of clicking.
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef;
const float incA = currentFreq / sampleRate; const float incA = currentFreq / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate; const float incB = (currentFreq * params.detuneRatio) / sampleRate;
@ -164,9 +186,24 @@ private:
float out = (1.0f - params.mix) * a + params.mix * b; 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); 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; float sampleRate = 44100.0f;
@ -184,6 +221,9 @@ private:
float env = 0.0f; float env = 0.0f;
Stage envStage = Stage::idle; Stage envStage = Stage::idle;
bool gate = false; bool gate = false;
float accentLevel = 1.0f;
float accentSmooth = 1.0f;
float accentSmoothCoef = 0.01f;
}; };
} // namespace monostep } // namespace monostep

View file

@ -2,6 +2,7 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <random>
namespace monostep namespace monostep
{ {
@ -11,7 +12,11 @@ enum class Waveform : int
sine = 0, sine = 0,
triangle = 1, triangle = 1,
saw = 2, saw = 2,
square = 3 square = 3,
pulse = 4,
noise = 5,
sub = 6,
pluck = 7
}; };
inline float frac (float x) 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); v -= polyBlep (frac (f + 0.5f), inc);
return v; 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; return 0.0f;

View file

@ -5,7 +5,7 @@
using namespace monostep; using namespace monostep;
SequencerMatrix::SequencerMatrix (MonoStepAudioProcessor& processorRef) SequencerMatrix::SequencerMatrix (MonostepAudioProcessor& processorRef)
: processor (processorRef) : processor (processorRef)
{ {
} }
@ -22,13 +22,19 @@ int SequencerMatrix::semitoneForRow (int row) const
juce::Rectangle<int> SequencerMatrix::matrixBounds() 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 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); return { juce::roundToInt (labelW), juce::roundToInt (headerH), gridW, gridH };
const int y = juce::roundToInt (headerH + (getHeight() - headerH - gridH) / 2.0f);
return { x, y, gridW, gridH };
} }
void SequencerMatrix::setSelectedStep (int stepIdx) void SequencerMatrix::setSelectedStep (int stepIdx)
@ -65,9 +71,16 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.fillRect (bounds.toFloat()); g.fillRect (bounds.toFloat());
// selected column highlight // 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()); 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 // grid lines
g.setColour (colours::grid); g.setColour (colours::grid);
@ -77,7 +90,7 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight()); 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; const float y = oy + i * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth()); 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.setColour ((i % 4 == 0 && i < seqLen) ? colours::text : colours::textDim);
g.setFont (font (10.0f)); 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); 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 // pitch labels
@ -152,10 +157,10 @@ void SequencerMatrix::paint (juce::Graphics& g)
const float radius = 3.0f; const float radius = 3.0f;
// cell body // cell body
juce::ColourGradient grad (isSelected ? colours::accent : colours::activeCell, juce::ColourGradient grad (colours::accent,
cell.getTopLeft(), cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
isSelected ? colours::accent2 : colours::activeCell.darker (0.6f), isSelected ? colours::accent.brighter (0.12f) : colours::accent.darker (0.35f),
cell.getBottomRight(), cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false); false);
g.setGradientFill (grad); g.setGradientFill (grad);
@ -215,6 +220,39 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.fillPath (tri); 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 --------------------------------- // --- dim columns beyond the pattern length ---------------------------------
if (seqLen < numSteps) if (seqLen < numSteps)
{ {
@ -222,21 +260,6 @@ void SequencerMatrix::paint (juce::Graphics& g)
g.fillRect (ox + seqLen * cellW, oy, g.fillRect (ox + seqLen * cellW, oy,
(numSteps - seqLen) * cellW, (float) bounds.getHeight()); (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 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()) const bool hit = bounds.contains (e.getPosition())
&& col >= 0 && col < numSteps && 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) void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
@ -260,11 +283,32 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
if (! cell.hit) if (! cell.hit)
return; return;
if (cell.row == -1)
{
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
return;
}
selectedStep = cell.step; selectedStep = cell.step;
if (onStepSelected) if (onStepSelected)
onStepSelected (cell.step); 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) if (cell.row == numRows)
{ {
slideDragging = true; slideDragging = true;
@ -299,13 +343,12 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e) void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
{ {
const auto cell = cellAt (e);
if (! cell.hit || cell.row == -1)
return;
if (fineDragging) if (fineDragging)
{ {
const auto cell = cellAt (e);
if (! cell.hit)
return;
// one matrix row (one semitone) maps to 4 cents of fine tuning // one matrix row (one semitone) maps to 4 cents of fine tuning
const float cents = juce::jlimit (-50.0f, 50.0f, const float cents = juce::jlimit (-50.0f, 50.0f,
fineStartCents + (fineRefRow - cell.row) * 4.0f); fineStartCents + (fineRefRow - cell.row) * 4.0f);
@ -325,8 +368,6 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
if (slideDragging) if (slideDragging)
{ {
const auto cell = cellAt (e);
if (cell.hit && cell.row == numRows && cell.step != slideLastStep) if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
{ {
slideLastStep = cell.step; slideLastStep = cell.step;
@ -340,10 +381,26 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
return; 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) if (! dragging)
return; return;
const auto cell = cellAt (e); if (cell.row >= numRows)
return;
if (cell.hit && cell.row < numRows && cell.row != lastRow) 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 int pitch = semitoneForRow (cell.row);
const auto& s = processor.getSequencer().step (cell.step);
if (startedActive) if (startedActive)
{ {
if (onNoteChanged) if (onNoteChanged)
@ -384,6 +443,12 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
return; return;
} }
if (accentDragging)
{
accentDragging = false;
return;
}
if (! dragging) if (! dragging)
return; return;
@ -391,7 +456,7 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
const auto cell = cellAt (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; return;
const int pitch = semitoneForRow (cell.row); const int pitch = semitoneForRow (cell.row);

View file

@ -2,12 +2,12 @@
#include <JuceHeader.h> #include <JuceHeader.h>
class MonoStepAudioProcessor; class MonostepAudioProcessor;
class SequencerMatrix final : public juce::Component class SequencerMatrix final : public juce::Component
{ {
public: public:
explicit SequencerMatrix (MonoStepAudioProcessor& processor); explicit SequencerMatrix (MonostepAudioProcessor& processor);
~SequencerMatrix() override = default; ~SequencerMatrix() override = default;
std::function<void (int stepIdx)> onStepSelected; std::function<void (int stepIdx)> onStepSelected;
@ -15,6 +15,7 @@ public:
std::function<void (int stepIdx, int semitone)> onNoteChanged; std::function<void (int stepIdx, int semitone)> onNoteChanged;
std::function<void (int stepIdx, float cents)> onFineChanged; std::function<void (int stepIdx, float cents)> onFineChanged;
std::function<void (int stepIdx, bool slide)> onSlideChanged; std::function<void (int stepIdx, bool slide)> onSlideChanged;
std::function<void (int stepIdx, bool accent)> onAccentChanged;
void setSelectedStep (int stepIdx); void setSelectedStep (int stepIdx);
int getSelectedStep() const { return selectedStep; } int getSelectedStep() const { return selectedStep; }
@ -39,14 +40,15 @@ private:
int rowForSemitone (int semitone) const; int rowForSemitone (int semitone) const;
int semitoneForRow (int row) const; int semitoneForRow (int row) const;
MonoStepAudioProcessor& processor; MonostepAudioProcessor& processor;
float cellW = 24.0f;
float cellH = 17.0f;
float labelW = 40.0f; float labelW = 40.0f;
float headerH = 24.0f; float headerH = 24.0f;
float headerPad = 10.0f; float headerPad = 10.0f;
mutable float cellW = 24.0f;
mutable float cellH = 17.0f;
int selectedStep = 0; int selectedStep = 0;
int playPos = -1; int playPos = -1;
@ -64,4 +66,8 @@ private:
bool slideDragging = false; bool slideDragging = false;
bool startedSlideActive = false; bool startedSlideActive = false;
int slideLastStep = -1; 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); const auto& sb = b.step (i);
if (sa.gate != sb.gate || sa.semitone != sb.semitone 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; return false;
} }
@ -63,7 +64,7 @@ static int testVst3Binary (const juce::File& pluginFile)
// Part B: drive the actual processor source and render audio. // Part B: drive the actual processor source and render audio.
static int testProcessorRendering() static int testProcessorRendering()
{ {
MonoStepAudioProcessor processor; MonostepAudioProcessor processor;
processor.prepareToPlay (44100.0, 512); processor.prepareToPlay (44100.0, 512);
const int totalSamples = (int) (44100.0 * 8.0); const int totalSamples = (int) (44100.0 * 8.0);
@ -96,7 +97,7 @@ static int testProcessorRendering()
} }
// Without a MIDI note the sequencer must stay silent. // Without a MIDI note the sequencer must stay silent.
MonoStepAudioProcessor idleProcessor; MonostepAudioProcessor idleProcessor;
idleProcessor.prepareToPlay (44100.0, 512); idleProcessor.prepareToPlay (44100.0, 512);
double idleSum = 0.0; double idleSum = 0.0;
@ -129,6 +130,7 @@ static int testProcessorRendering()
processor.setStepNote (0, 7); processor.setStepNote (0, 7);
processor.setStepCents (0, 42.0f); processor.setStepCents (0, 42.0f);
processor.setStepSlide (1, true); processor.setStepSlide (1, true);
processor.setStepAccent (0, true);
if (std::fabs (processor.getSequencer().step (0).cents - 42.0f) > 0.001f) if (std::fabs (processor.getSequencer().step (0).cents - 42.0f) > 0.001f)
{ {
@ -136,12 +138,97 @@ static int testProcessorRendering()
return 1; 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 // state round trip through a fresh instance
juce::MemoryBlock state; juce::MemoryBlock state;
processor.getStateInformation (state); processor.getStateInformation (state);
std::cout << "State bytes: " << (int) state.getSize() << "\n"; std::cout << "State bytes: " << (int) state.getSize() << "\n";
MonoStepAudioProcessor processor2; MonostepAudioProcessor processor2;
processor2.prepareToPlay (44100.0, 512); processor2.prepareToPlay (44100.0, 512);
processor2.setStateInformation (state.getData(), (int) state.getSize()); processor2.setStateInformation (state.getData(), (int) state.getSize());
@ -152,7 +239,8 @@ static int testProcessorRendering()
std::cout << "step " << i << ": gate=" << a.gate << "/" << b.gate std::cout << "step " << i << ": gate=" << a.gate << "/" << b.gate
<< " note=" << a.semitone << "/" << b.semitone << " note=" << a.semitone << "/" << b.semitone
<< " cents=" << a.cents << "/" << b.cents << " 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())) if (! patternsEqual (processor.getSequencer(), processor2.getSequencer()))
@ -218,7 +306,7 @@ int main (int argc, char** argv)
{ {
if (argc < 2) if (argc < 2)
{ {
std::cout << "Usage: MonoStepTestHost <plugin.vst3> [out.wav]\n"; std::cout << "Usage: MonostepTestHost <plugin.vst3> [out.wav]\n";
return 1; return 1;
} }