From ab9ea542a7547399394527ac588acf4858a6d6db Mon Sep 17 00:00:00 2001 From: Armin Date: Sat, 12 Sep 2026 21:53:16 +0200 Subject: [PATCH] Fetch JUCE into build/, tidy gitignore, expand README --- .gitignore | 72 +++++++++++-- CMakeLists.txt | 4 +- Makefile | 2 +- README.md | 108 ++++++++++++++------ Source/LcdDisplay.cpp | 204 +++++++++++++++++++++++++------------ Source/LcdDisplay.h | 14 +-- Source/PluginEditor.cpp | 50 +++++++-- Source/PluginEditor.h | 19 +++- Source/PluginProcessor.cpp | 2 +- Source/Presets.cpp | 2 +- Source/SynthEngine.cpp | 20 ++-- Source/SynthParams.cpp | 2 +- 12 files changed, 350 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index 07f9b35..94760c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,67 @@ -build/ -deps/ -*.o -*.d -*.dylib +# Build output (all build products, including the JUCE dep fetched into build/) +/build/ + +# ===== Editors / text tools ===== +# Vim swap + backup files +*.swp +*.swo +*.swn +*.swx +*~ +*.~[0-9]* +# Vim persistent undo history +*.un~ + +# Backup / miscellaneous editor files +*.bak +*.backup +*.old +*.orig +*.rej +.patch + +# IDE / editor config +.idea/ +.vscode/ +*.code-workspace .DS_Store -*.dSYM/ -*.vst3/ -*.component/ + +# ===== CMake / build system ===== CMakeCache.txt CMakeFiles/ Makefile.cmake -JuceLibraryCode/ \ No newline at end of file +*.cmake.in +cmake_install.cmake +CTestTestfile.cmake +Testing/ + +# JUCE generated sources outside build/ +JuceLibraryCode/ +juce_build/ + +# ===== C / C++ object + coverage artifacts ===== +*.o +*.obj +*.a +*.lib +*.so +*.dylib +*.dll +*.d +*.gcda +*.gcno +*.profraw + +# ===== Plugin/audio output bundles ===== +*.vst3/ +*.component/ +*.bundle/ +*.app/ +*.dSYM/ +*.lv2/ + +# ===== Logs / transient ===== +*.log +core +core.* +*.pid \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a6aeaed..1e7972a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) if(NOT DEFINED JUCE_ROOT) - set(JUCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/deps/juce") + set(JUCE_ROOT "${CMAKE_CURRENT_BINARY_DIR}/juce") endif() if(NOT DEFINED CMAKE_BUILD_TYPE) @@ -14,7 +14,7 @@ if(NOT DEFINED CMAKE_BUILD_TYPE) endif() if(NOT EXISTS "${JUCE_ROOT}/CMakeLists.txt") - message(FATAL_ERROR "JUCE not found at ${JUCE_ROOT}. Run 'make deps' first.") + message(FATAL_ERROR "JUCE not found at ${JUCE_ROOT}. Run 'make deps' first (clones JUCE into build/).") endif() add_subdirectory("${JUCE_ROOT}" juce_build) diff --git a/Makefile b/Makefile index a840148..42116a7 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ SHELL := /bin/sh UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) -JUCE_ROOT ?= deps/juce BUILD_DIR ?= build +JUCE_ROOT ?= $(BUILD_DIR)/juce CONFIG := Release JOBS := $(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) diff --git a/README.md b/README.md index c6a8896..ea2c2b0 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,100 @@ # SM-26 -A simple subtractive synthesizer plugin built with JUCE / C++. +A self-contained **subtractive synthesizer** written in modern C++ on top of +[JUCE](https://juce.com). It ships as a **VST3** on macOS and Linux plus a +macOS **Audio Unit**, with a hand-rolled DSP core, 52 factory presets and an +embedded 4x80 HD44780-style LCD driven by a cascaded on-screen menu. -* **VST3** + **Audio Unit** (macOS arm64), **VST3** (Linux) -* 4 rotary knobs on the front panel whose meaning follows the selected preset -* 52 factory presets (Bass / Lead / Pad / Keys / Pluck / FX / Stab / Arp) -* tempo-synced delay (follows the host transport / BPM) -* 4x80 HD44780-style pixel display with a cascaded on-screen menu -* left/right/up/down navigation diamond + ENTER / ESC controls the menu -* status footer with the git revision and local build time +## Why this is pretty good + +* **One real dependency, always fetchable.** JUCE is the only external thing + this project needs, and the build fetches it automatically into `./build/` + on the first `make`. Your source tree stays clean: no vendored JUCE, no + submodules to go stale, no manual setup. +* **A tiny, readable DSP core with real character.** Twelve voices, two + oscillators with gliding, a self-oscillating state-variable filter with + drive, an ADSR, and a tempo-synced delay — all in one focused + `SynthEngine` with no external DSP libraries. Easy to read, easy to hack. +* **A UI people actually look at.** A 4x80 pixel LCD renderer with a real + menu (PRESETS / OSCILLATORS / FILTER / ENVELOPE / DELAY / SYSTEM) navigated + with a diamond of arrow keys plus ENTER/ESC. A DAW-native plugin that wears + its own little OS. +* **Knobs that mean something.** Four front-panel knobs that change meaning + per preset (`KnobView`) — every preset ships with a curated mapping and a + sensible value range, not just raw MIDI CCs. +* **Reproducible from a single command.** `make` = fetch JUCE → configure → + build → you're done. `make install` even ad-hoc-signs and un-quarantines + the macOS bundles so your DAW and `auval` accept them immediately. + +## Features + +* 12-voice polyphonic engine with voice stealing and glide +* 52 factory presets across Bass / Lead / Pad / Keys / Pluck / FX / Stab / Arp +* Dual-waveform oscillator section (sine / tri / saw / square / noise …) +* SVF low-pass filter with resonance **and** drive +* ADSR with a per-voice envelope and cutoff modulation +* Tempo-synced delay with rhythmic subdivision, locked to host BPM +* 4x80 LCD with live knob values, active preset, voice count and transport +* Footer that shows the exact git revision and build time + +## Requirements + +* C++17 compiler (Apple Clang or GCC) +* CMake >= 3.22 +* Git (to fetch JUCE on the first build) +* Linux: X11/ALSA development headers ## Build -Requires a C++17 compiler, CMake >= 3.22 and (on Linux) X11/ALSA dev headers. - ```sh -make # clones JUCE (once) into deps/ and builds -make install # installs + ad-hoc signs + un-quarantines +make # fetches JUCE into build/ (once), configures and builds +make install # installs + ad-hoc signs + un-quarantines the plugin bundles make uninstall # removes the installed plugins -make distclean # wipes build/ +make clean # removes compiled artefacts only +make distclean # wipes build/ entirely (including the fetched JUCE) ``` -* macOS installs to `~/Library/Audio/Plug-Ins/` (AU + VST3). +* macOS installs to `~/Library/Audio/Plug-Ins/` (AU + **VST3**). * Linux installs to `~/.vst3/`. +> Jumping straight into CMake also works: `cmake -S . -B build` builds a +> Release plugin and grabs JUCE from the same `build/juce` location. + If a freshly installed AU is not visible to `auval` / your DAW: ```sh sudo killall -HUP coreaudiod ``` +(or just restart the host). + ## Usage -* Ship MIDI into the plugin; the 4 front-panel knobs drive whichever - parameters the current preset maps them to (e.g. cutoff/resonance/ - envelope/delay, but many other routings exist). -* **Nav diamond + ENTER/ESC** drive the on-screen menu: - * root menu -> PRESETS / OSCILLATORS / FILTER / ENVELOPE / DELAY / SYSTEM - * up/down = move, left/right = adjust value, ENTER = open/confirm, ESC = back -* The LCD shows the current preset, the four knob mappings with live values, - host BPM and voice count. The delay can be set to a rhythmic subdivision - (SYNC) and locks to the host tempo. +* Feed the plugin MIDI; the four front-panel knobs drive whichever parameters + the current preset maps them to (cutoff, resonance, envelope, delay — and a + wide range of other routings). +* Navigate with the **arrow diamond + ENTER/ESC**: + * root → PRESETS / OSCILLATORS / FILTER / ENVELOPE / DELAY / SYSTEM + * up/down move, left/right adjust, ENTER opens/confirms, ESC goes back +* The LCD shows the active preset, knob mappings with live values, host BPM + and voice count. The delay's SYNC mode locks to the host tempo and lets you + pick rhythmic subdivisions. ## Layout ``` -Build: Makefile -> CMake -> JUCE (deps/juce) +Build: Makefile -> CMake -> JUCE (fetched into build/juce, never committed) Source/ - PluginProcessor.* audio processor + DSP shell - SynthEngine.* osc/filter/ADSR/delay voice engine - Presets.* 52 preset definitions + knob routings - SynthParams.* parameter model + formatting - PluginEditor.* panel, knobs, nav diamond, footer - LcdDisplay.* 4x80 HD44780-style display renderer - MenuSystem.* cascaded menu state machine -tools/gen_buildinfo.sh generates git-version + build-time header -``` \ No newline at end of file + PluginProcessor.* audio processor + DSP shell + SynthEngine.* osc/filter/ADSR/delay voice engine (12 voices) + Presets.* 52 preset definitions + per-preset knob routings + SynthParams.* parameter model + formatting + PluginEditor.* panel, knobs, nav diamond, status footer + LcdDisplay.* 4x80 HD44780-style display renderer + MenuSystem.* cascaded menu state machine +tools/gen_buildinfo.sh generates git-revision + build-time header +``` + +Everything the project needs at build time lives under `./build/`, which is +git-ignored — the only things committed are your sources, this README and +the build glue. \ No newline at end of file diff --git a/Source/LcdDisplay.cpp b/Source/LcdDisplay.cpp index 2225221..d770522 100644 --- a/Source/LcdDisplay.cpp +++ b/Source/LcdDisplay.cpp @@ -1,66 +1,116 @@ #include "LcdDisplay.h" +namespace { + +// Classic 5x8 dot-matrix LCD character generator ROM (HD44780-style), +// rows top-to-bottom, bit 4 = leftmost column. Index = ASCII - 32. +constexpr uint8_t kFont[95][8] = +{ + {0,0,0,0,0,0,0,0}, // space + {4,4,4,4,4,0,4,0}, // ! + {10,10,10,0,0,0,0,0}, // " + {0,10,31,10,31,10,0,0}, // # + {4,15,20,14,5,30,4,0}, // $ + {2,4,8,16,14,17,14,0}, // % + {8,20,20,8,21,18,13,0}, // & + {4,4,4,0,0,0,0,0}, // ' + {2,4,8,8,8,4,2,0}, // ( + {8,4,2,2,2,4,8,0}, // ) + {0,10,4,31,4,10,0,0}, // * + {0,4,4,31,4,4,0,0}, // + + {0,0,0,0,6,2,4,0}, // , + {0,0,0,31,0,0,0,0}, // - + {0,0,0,0,0,0,12,0}, // . + {1,2,4,8,16,0,0,0}, // / + {14,17,19,21,25,17,14,0}, // 0 + {4,12,4,4,4,4,14,0}, // 1 + {14,17,1,2,4,8,31,0}, // 2 + {14,17,1,6,1,17,14,0}, // 3 + {2,6,10,18,31,2,2,0}, // 4 + {31,16,30,1,1,17,14,0}, // 5 + {6,8,16,30,17,17,14,0}, // 6 + {31,1,2,4,8,8,8,0}, // 7 + {14,17,17,14,17,17,14,0}, // 8 + {14,17,17,15,1,2,12,0}, // 9 + {0,0,6,0,0,6,0,0}, // : + {0,0,6,0,0,6,2,4}, // ; + {2,4,8,16,8,4,2,0}, // < + {0,0,31,0,31,0,0,0}, // = + {8,4,2,1,2,4,8,0}, // > + {14,17,1,2,4,0,4,0}, // ? + {14,17,1,13,21,17,14,0}, // @ + {14,17,17,31,17,17,17,0}, // A + {30,17,17,30,17,17,30,0}, // B + {14,17,16,16,16,17,14,0}, // C + {30,17,17,17,17,17,30,0}, // D + {31,16,16,30,16,16,31,0}, // E + {31,16,16,30,16,16,16,0}, // F + {14,17,16,23,17,17,15,0}, // G + {17,17,17,31,17,17,17,0}, // H + {14,4,4,4,4,4,14,0}, // I + {7,2,2,2,2,18,12,0}, // J + {17,18,20,24,20,18,17,0}, // K + {16,16,16,16,16,16,31,0}, // L + {17,27,21,21,17,17,17,0}, // M + {17,25,21,19,17,17,17,0}, // N + {14,17,17,17,17,17,14,0}, // O + {30,17,17,30,16,16,16,0}, // P + {14,17,17,17,21,18,13,0}, // Q + {30,17,17,30,20,18,17,0}, // R + {15,16,16,14,1,1,30,0}, // S + {31,4,4,4,4,4,4,0}, // T + {17,17,17,17,17,17,14,0}, // U + {17,17,17,17,17,10,4,0}, // V + {17,17,17,21,21,27,17,0}, // W + {17,17,10,4,10,17,17,0}, // X + {17,17,17,10,4,4,4,0}, // Y + {31,1,2,4,8,16,31,0}, // Z + {14,8,8,8,8,8,14,0}, // [ + {16,8,4,2,1,0,0,0}, // backslash + {14,2,2,2,2,2,14,0}, // ] + {4,10,17,0,0,0,0,0}, // ^ + {0,0,0,0,0,0,31,0}, // _ + {8,4,2,0,0,0,0,0}, // ` + {0,0,14,1,15,17,15,0}, // a + {16,16,30,17,17,17,30,0}, // b + {0,0,14,16,16,17,14,0}, // c + {1,1,15,17,17,17,15,0}, // d + {0,0,14,17,31,16,14,0}, // e + {2,4,4,14,4,4,4,0}, // f + {0,0,15,17,17,15,1,14}, // g + {16,16,30,17,17,17,17,0}, // h + {0,6,0,4,4,4,4,0}, // i + {0,6,0,4,4,4,8,0}, // j + {0,0,16,18,20,24,20,0}, // k + {0,0,12,4,4,4,4,0}, // l + {0,0,26,21,21,21,21,0}, // m + {0,0,18,29,17,17,17,0}, // n + {0,0,14,17,17,17,14,0}, // o + {0,30,17,17,30,16,16,0}, // p + {0,0,15,17,17,15,1,1}, // q + {0,0,30,17,16,16,16,0}, // r + {0,0,15,16,14,1,30,0}, // s + {0,4,4,14,4,4,2,0}, // t + {0,0,17,17,17,19,13,0}, // u + {0,0,17,17,17,10,4,0}, // v + {0,0,17,17,21,21,10,0}, // w + {0,0,17,10,4,10,17,0}, // x + {0,0,17,17,17,10,12,0}, // y + {0,0,31,2,4,8,31,0}, // z + {4,8,8,16,8,8,4,0}, // { + {4,4,4,4,4,4,4,0}, // | + {8,4,4,2,4,4,8,0}, // } + {13,18,0,0,0,0,0,0}, // ~ +}; + +} // namespace + LcdDisplay::LcdDisplay() { clear(); startTimerHz (30); } -void LcdDisplay::rasterize (juce::uint32 c) -{ - if (c >= 256 || glyphCache[c].cached) return; - - juce::Image img (juce::Image::ARGB, kRasterW, kRasterH, true); - { - juce::Graphics g (img); - g.setColour (juce::Colours::white); - g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), - 14.0f, juce::Font::plain })); - juce::String s; - s << (juce_wchar) c; - g.drawText (s, img.getBounds().removeFromBottom (kRasterH - 2), - juce::Justification::centredBottom, false); - } - - const float sx = (float) kRasterW / 5.0f; - const float sy = (float) kRasterH / 8.0f; - - for (int row = 0; row < 8; ++row) - glyphCache[c].rows[row] = 0; - - for (int row = 0; row < 8; ++row) - { - uint8_t bits = 0; - for (int col = 0; col < 5; ++col) - { - int count = 0, total = 0; - int px0 = (int) (col * sx); - int py0 = (int) (row * sy); - int px1 = juce::jmin ((int) ((col + 1) * sx) - 1, kRasterW - 1); - int py1 = juce::jmin ((int) ((row + 1) * sy) - 1, kRasterH - 1); - for (int y = py0; y <= py1; ++y) - for (int x = px0; x <= px1; ++x) - { - total++; - if (img.getPixelAt (x, y).getAlpha() > 127) - count++; - } - if (total > 0 && count > total / 3) - bits |= (uint8_t) (1u << (4 - col)); - } - glyphCache[c].rows[row] = bits; - } - glyphCache[c].cached = true; -} - -uint8_t* LcdDisplay::getGlyph (juce::uint32 c) -{ - if (c >= 256) c = '?'; - if (! glyphCache[c].cached) - rasterize (c); - return glyphCache[c].rows; -} - void LcdDisplay::setLine (int row, const juce::String& s) { if (row >= 0 && row < kRows) @@ -78,6 +128,13 @@ void LcdDisplay::clear() cursorVisible = false; } +const uint8_t* LcdDisplay::getGlyph (juce::uint32 c) const +{ + if (c < 32 || c > 126) + c = '?'; + return kFont[c - 32]; +} + void LcdDisplay::paint (juce::Graphics& g) { const float w = (float) getWidth(); @@ -95,6 +152,22 @@ void LcdDisplay::paint (juce::Graphics& g) float ox = 0.0f; float oy = 0.0f; + float padX = cw * 0.18f; + float padY = ch * 0.10f; + float gw = cw - padX * 2.0f; + float gh = ch - padY * 2.0f; + + constexpr float kAspect = 2.0f; + constexpr float kGapCol = 0.22f; + constexpr float kGapRow = 0.12f; + + const float gridW = 5.0f + 4.0f * kGapCol; + const float gridH = kAspect * (8.0f + 7.0f * kGapRow); + const float cellW = juce::jmax (0.0f, juce::jmin (gw / gridW, gh / gridH)); + const float cellH = cellW * kAspect; + const float stepX = cellW * (1.0f + kGapCol); + const float stepY = cellH * (1.0f + kGapRow); + bool blinkOn = cursorVisible && ((int) (juce::Time::getMillisecondCounterHiRes() / 400.0) % 2 == 0); @@ -104,7 +177,7 @@ void LcdDisplay::paint (juce::Graphics& g) for (int col = 0; col < kCols; ++col) { juce_wchar ch2 = col < line.length() ? line [(size_t) col] : (juce_wchar) ' '; - uint8_t* glyph = getGlyph ((juce::uint32) ch2); + const uint8_t* glyph = getGlyph ((juce::uint32) ch2); float cx = ox + (float) col * cw; float cy = oy + (float) row * ch; @@ -112,10 +185,8 @@ void LcdDisplay::paint (juce::Graphics& g) g.setColour (pixOff); g.fillRect (cx + 0.5f, cy + 0.5f, cw - 1.0f, ch - 1.0f); - float padX = cw * 0.12f; - float padY = ch * 0.1f; - float gw = cw - padX * 2.0f; - float gh = ch - padY * 2.0f; + float gridX = cx + padX + (gw - cellW * gridW) * 0.5f; + float gridY = cy + padY + (gh - cellW * gridH) * 0.5f; for (int gRow = 0; gRow < 8; ++gRow) { @@ -125,15 +196,14 @@ void LcdDisplay::paint (juce::Graphics& g) { if (bits & (1u << (4 - gCol))) { - float px = cx + padX + (float) gCol * gw / 5.0f; - float py = cy + padY + (float) gRow * gh / 8.0f; - float pw = gw / 5.0f - 0.5f; - float ph = gh / 8.0f - 0.4f; + float px = gridX + (float) gCol * stepX; + float py = gridY + (float) gRow * stepY; g.setColour (pixOnDim); - g.fillRect (px - 0.6f, py - 0.4f, pw + 1.2f, ph + 0.8f); + g.fillRect (px - cellW * 0.10f, py - cellH * 0.05f, + cellW * 1.20f, cellH * 1.10f); g.setColour (pixOn); - g.fillRect (px, py, pw, ph); + g.fillRect (px, py, cellW, cellH); } } } @@ -147,4 +217,4 @@ void LcdDisplay::paint (juce::Graphics& g) } } -void LcdDisplay::timerCallback() { repaint(); } +void LcdDisplay::timerCallback() { repaint(); } \ No newline at end of file diff --git a/Source/LcdDisplay.h b/Source/LcdDisplay.h index 379af74..7192352 100644 --- a/Source/LcdDisplay.h +++ b/Source/LcdDisplay.h @@ -19,24 +19,12 @@ public: private: void timerCallback() override; - static constexpr int kRasterW = 12; - static constexpr int kRasterH = 18; - juce::String lines[kRows]; int cursorCol = 0; int cursorRow = 0; bool cursorVisible = false; - struct GlyphCache { - uint8_t rows[8] = {}; - bool cached = false; - }; - - GlyphCache glyphCache[256]; - - void rasterize (juce::uint32 c); - uint8_t* getGlyph (juce::uint32 c); - juce::Font getMonoFont() const; + const uint8_t* getGlyph (juce::uint32 c) const; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LcdDisplay) }; \ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index f93c68e..7dd8495 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -68,10 +68,17 @@ void SM26AudioProcessorEditor::setupKnobs() } } +juce::Font SM26AudioProcessorEditor::NavButtonLookAndFeel::getTextButtonFont (juce::TextButton&, int) +{ + return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), + 10.5f, juce::Font::bold }); +} + void SM26AudioProcessorEditor::setupNavButtons() { auto setup = [this](juce::TextButton& btn, juce::Colour bg) { + btn.setLookAndFeel (&navLookAndFeel); btn.setColour (juce::TextButton::buttonColourId, bg); btn.setColour (juce::TextButton::textColourOffId, juce::Colour (0xFFE0E0C0)); btn.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A4A18)); @@ -85,14 +92,22 @@ void SM26AudioProcessorEditor::setupNavButtons() setup (navEnter, juce::Colour (0xFF3A6A28)); setup (navEsc, juce::Colour (0xFF3A2A18)); - navUp.onClick = [this] { menu.up(); }; - navDown.onClick = [this] { menu.down(); }; + navUp.onClick = [this] { if (menu.isOpen()) menu.up(); else cyclePreset (-1); }; + navDown.onClick = [this] { if (menu.isOpen()) menu.down(); else cyclePreset (+1); }; navLeft.onClick = [this] { menu.left(); }; navRight.onClick = [this] { menu.right(); }; navEnter.onClick = [this] { menu.open(); }; navEsc.onClick = [this] { if (menu.isOpen()) menu.escape(); else menu.close(); }; } +void SM26AudioProcessorEditor::cyclePreset (int delta) +{ + const int count = sm26::PresetStore::count(); + if (count <= 0) return; + const int idx = proc.engine.presetIndex(); + proc.setPresetByIndex ((idx + delta + count) % count); +} + void SM26AudioProcessorEditor::buildRootMenu() { auto builder = [this]() -> MenuScreen @@ -356,7 +371,7 @@ void SM26AudioProcessorEditor::buildRootMenu() } { MenuItem m2; - m2.label = "48 presets | VST3 + AU"; + m2.label = "52 presets | VST3 + AU"; m2.kind = MenuItem::Action; as.items.push_back (m2); } @@ -463,7 +478,7 @@ void SM26AudioProcessorEditor::resized() float knobGap = 18.0f; float knobsW = 4.0f * knobSize + 3.0f * knobGap; float knobsX = (totalW - knobsW) / 2.0f; - float knobsY = totalH - 135.0f; + float knobsY = juce::jmax (190.0f, totalH - 265.0f); for (int i = 0; i < 4; ++i) { @@ -504,7 +519,11 @@ void SM26AudioProcessorEditor::timerCallback() { auto kv = proc.engine.knobView (i); float val = proc.engine.getParam (kv.param); - knobSliders[i].setValue (val, juce::dontSendNotification); + if (! knobDragging[i]) + { + float pos = sm26::clamp01 ((val - kv.lo) / (kv.hi - kv.lo + 1e-9f)); + knobSliders[i].setValue (pos, juce::dontSendNotification); + } knobLabels[i].setText (kv.name, juce::dontSendNotification); knobValueLabels[i].setText (sm26::formatParamReal (kv.param, val), juce::dontSendNotification); } @@ -526,11 +545,27 @@ void SM26AudioProcessorEditor::sliderValueChanged (juce::Slider* s) auto kv = proc.engine.knobView (i); float paramVal = sm26::clamp01 (kv.lo + v * (kv.hi - kv.lo)); proc.engine.setParam (kv.param, paramVal); + knobValueLabels[i].setText (sm26::formatParamReal (kv.param, paramVal), + juce::dontSendNotification); break; } } } +void SM26AudioProcessorEditor::sliderDragStarted (juce::Slider* s) +{ + for (int i = 0; i < 4; ++i) + if (s == &knobSliders[i]) + knobDragging[i] = true; +} + +void SM26AudioProcessorEditor::sliderDragEnded (juce::Slider* s) +{ + for (int i = 0; i < 4; ++i) + if (s == &knobSliders[i]) + knobDragging[i] = false; +} + void SM26AudioProcessorEditor::updateLCD() { if (! menu.isOpen()) @@ -563,7 +598,8 @@ void SM26AudioProcessorEditor::updateLCD() int notes = proc.engine.notesPlaying(); juce::String notesStr = notes > 0 ? "NOTES:" + juce::String (notes) : ""; - lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr); + lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr + + " UP/DN=PATCH OK=MENU"); lcd.showCursor (false); } else @@ -582,7 +618,7 @@ void SM26AudioProcessorEditor::updateLCD() line = line + " [ENTER]"; lcd.setLine (1 + row, line); } - lcd.setLine (3, "UP/DOWN=SEL L/R=VAL ENTER=OK ESC=BACK"); + lcd.setLine (3, "UP/DN=SEL L/R=VAL OK=OK ESC=BACK"); lcd.showCursor (false); } } \ No newline at end of file diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 7726311..0c910ab 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -21,26 +21,37 @@ public: private: void sliderValueChanged (juce::Slider*) override; + void sliderDragStarted (juce::Slider*) override; + void sliderDragEnded (juce::Slider*) override; void setupKnobs(); void setupNavButtons(); void buildRootMenu(); void updateLCD(); + void cyclePreset (int delta); SM26AudioProcessor& proc; MenuSystem menu; + class NavButtonLookAndFeel : public juce::LookAndFeel_V4 + { + public: + juce::Font getTextButtonFont (juce::TextButton&, int) override; + } navLookAndFeel; + LcdDisplay lcd; + bool knobDragging[4] = {}; + juce::Slider knobSliders[4]; juce::Label knobLabels[4]; juce::Label knobValueLabels[4]; juce::TextButton navUp { "UP" }; - juce::TextButton navDown { "DOWN" }; - juce::TextButton navLeft { "LEFT" }; - juce::TextButton navRight { "RIGHT" }; - juce::TextButton navEnter { "ENTER" }; + juce::TextButton navDown { "DN" }; + juce::TextButton navLeft { "LT" }; + juce::TextButton navRight { "RT" }; + juce::TextButton navEnter { "OK" }; juce::TextButton navEsc { "ESC" }; juce::Label footer; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index f6d6e19..b0fc6ff 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -131,7 +131,7 @@ void SM26AudioProcessor::parameterChanged (const juce::String& parameterID, floa if (parameterID == "preset") { - int idx = (int) std::round (newValue * (float) (sm26::PresetStore::count() - 1)); + int idx = juce::roundToInt (newValue); idx = juce::jlimit (0, sm26::PresetStore::count() - 1, idx); engine.setPresetIndex (idx); applyPresetKnobs(); diff --git a/Source/Presets.cpp b/Source/Presets.cpp index 0d5c34a..9ad0e86 100644 --- a/Source/Presets.cpp +++ b/Source/Presets.cpp @@ -5,7 +5,7 @@ namespace sm26 { namespace { struct Catalog { - Preset data[48]; + Preset data[52]; int n = 0; }; diff --git a/Source/SynthEngine.cpp b/Source/SynthEngine.cpp index 0755e64..13cb914 100644 --- a/Source/SynthEngine.cpp +++ b/Source/SynthEngine.cpp @@ -53,6 +53,7 @@ void SynthEngine::loadPreset (const Preset& p) knobHighs[i] = p.ctl[i].hi; } allNotesOff(); + delay.clear(); } void SynthEngine::setPresetIndex (int i) @@ -243,14 +244,14 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, const int filterType = paramFilterType (ftypeN01); bool delaySync = params[PDSYNC] > 0.5f; - int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS] * 15.0f)); + int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS])); float delayMsParam = paramReal (PDTIME, params[PDTIME]); float delaySec; if (delaySync) delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue); else delaySec = delayMsParam * 0.001f; - delaySec = std::clamp (delaySec, 0.001f, 17.0f); + delaySec = std::clamp (delaySec, 0.001f, 4.0f); lastDelaySamples = delaySec * sampleRate; const float delayFB = params[PDFB]; @@ -258,18 +259,16 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, const float dryMix = 1.0f - delayMix; const float wetMix = delayMix; - int delayWrite = (int) delay.writePos[0]; - int delayRead = (delayWrite - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen; - (void) delayRead; - for (const auto& metadata : midi) { MidiMessage msg = metadata.getMessage(); - int sm = metadata.samplePosition; - if (sm < n && msg.isNoteOn()) + int sm = juce::jlimit (0, n - 1, metadata.samplePosition); + if (msg.isNoteOn()) noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); - else if (sm < n && msg.isNoteOff()) + else if (msg.isNoteOff()) noteOff (msg.getNoteNumber()); + else if (msg.isAllNotesOff() || msg.isAllSoundOff()) + allNotesOff(); } float* outL = buffer.getWritePointer (0); @@ -339,7 +338,8 @@ void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, float dry = sumL; int w = delay.writePos[0]; - float delayed = delay.buf[0][w]; + int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen; + float delayed = delay.buf[0][r]; float fb = dry + delayed * (float) delayFB; fb = softClip (fb * 0.7f); delay.buf[0][w] = fb; diff --git a/Source/SynthParams.cpp b/Source/SynthParams.cpp index 66da20d..178fe56 100644 --- a/Source/SynthParams.cpp +++ b/Source/SynthParams.cpp @@ -178,7 +178,7 @@ String formatParamReal(int id, float n01) return formatTimeMs (paramReal (id, n01) * 1000.0f); case PDTIME: return formatTimeMs (paramReal (id, n01)); case PDSTEPS: - return stepsLabels() [juce::jlimit (0, 15, (int) juce::roundToInt (n01 * 15.0f))]; + return stepsLabels() [juce::jlimit (0, 15, (int) juce::roundToInt (n01))]; case PDSYNC: return n01 > 0.5f ? "SYNC" : "MS"; case PGLIDE: