mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
Fetch JUCE into build/, tidy gitignore, expand README
This commit is contained in:
parent
4a761729f8
commit
ab9ea542a7
12 changed files with 350 additions and 149 deletions
72
.gitignore
vendored
72
.gitignore
vendored
|
|
@ -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/
|
||||
*.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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
2
Makefile
2
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)
|
||||
|
||||
|
|
|
|||
108
README.md
108
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
|
||||
```
|
||||
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.
|
||||
|
|
@ -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(); }
|
||||
|
|
@ -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)
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace sm26 {
|
|||
namespace {
|
||||
|
||||
struct Catalog {
|
||||
Preset data[48];
|
||||
Preset data[52];
|
||||
int n = 0;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue