Fetch JUCE into build/, tidy gitignore, expand README

This commit is contained in:
Armin 2026-09-12 21:53:16 +02:00
commit ab9ea542a7
12 changed files with 350 additions and 149 deletions

70
.gitignore vendored
View file

@ -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
*.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

View file

@ -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)

View file

@ -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)

View file

@ -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
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, footer
PluginEditor.* panel, knobs, nav diamond, status footer
LcdDisplay.* 4x80 HD44780-style display renderer
MenuSystem.* cascaded menu state machine
tools/gen_buildinfo.sh generates git-version + build-time header
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.

View file

@ -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);
}
}
}

View file

@ -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)
};

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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();

View file

@ -5,7 +5,7 @@ namespace sm26 {
namespace {
struct Catalog {
Preset data[48];
Preset data[52];
int n = 0;
};

View file

@ -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<float>& 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<float>& 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<float>& 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;

View file

@ -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: