mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
Prototype
This commit is contained in:
parent
c117906a7f
commit
4271fc34bb
14 changed files with 950 additions and 169 deletions
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
10
Makefile
10
Makefile
|
|
@ -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:
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
class MonoStepAudioProcessorEditor final : public juce::AudioProcessorEditor,
|
juce::ToggleButton randomizeToggles[RandomGroupCount];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -77,10 +94,14 @@ private:
|
||||||
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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -298,14 +342,13 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
|
||||||
}
|
}
|
||||||
|
|
||||||
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
||||||
{
|
|
||||||
if (fineDragging)
|
|
||||||
{
|
{
|
||||||
const auto cell = cellAt (e);
|
const auto cell = cellAt (e);
|
||||||
|
if (! cell.hit || cell.row == -1)
|
||||||
if (! cell.hit)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (fineDragging)
|
||||||
|
{
|
||||||
// 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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue