Add delay tempo sync, rack faceplate, README, docs

- Add host-tempo-synced delay: FX screen gains a 4th SYNC row for
  rhythmic divisions (1/16 .. 1/1) that lock delay time to DAW tempo
- Embed rack faceplate background (b2.png) via juce binary data
- Add offline dev probe (rmx_probe) for GUI-less MIDI testing
- Allow overriding JUCE_ROOT when configuring the build
- Write comprehensive README describing the instrument, UI, MIDI, DSP,
  and build/install steps
This commit is contained in:
Armin 2026-09-08 15:10:12 +02:00
commit 2e870380d4
17 changed files with 1413 additions and 324 deletions

View file

@ -54,10 +54,12 @@ message(STATUS "RMX-19 build: ${BUILD_GIT_BRANCH} ${BUILD_GIT_COMMIT}${BUILD_GIT
# ---------------------------------------------------------------------------
# JUCE
# ---------------------------------------------------------------------------
if(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt")
if(NOT DEFINED JUCE_ROOT)
if(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt")
set(JUCE_ROOT "$ENV{HOME}/JUCE")
elseif(EXISTS "/opt/JUCE/CMakeLists.txt")
elseif(EXISTS "/opt/JUCE/CMakeLists.txt")
set(JUCE_ROOT "/opt/JUCE")
endif()
endif()
if(NOT DEFINED JUCE_ROOT OR NOT EXISTS "${JUCE_ROOT}/CMakeLists.txt")
@ -92,15 +94,42 @@ set(RMX19_SOURCES
Source/Params.cpp
)
juce_add_binary_data(RMX19_Assets
NAMESPACE BinaryData
SOURCES
b2.png
)
target_sources(RMX19 PRIVATE ${RMX19_SOURCES})
target_include_directories(RMX19 PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/Source"
"${CMAKE_CURRENT_BINARY_DIR}/Source")
target_link_libraries(RMX19 PRIVATE
RMX19_Assets
juce::juce_audio_utils
juce::juce_gui_basics
juce::juce_dsp)
target_compile_definitions(RMX19 PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1
JUCE_VST3_CAN_REPLACE_VST2=0)
# ---------------------------------------------------------------------------
# Offline probe (dev only): runs the processor with scripted MIDI, no GUI.
# ---------------------------------------------------------------------------
add_executable(rmx_probe Source/Probe.cpp
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/LcdPanel.cpp
Source/PresetBank.cpp
Source/SynthEngine.cpp
Source/Params.cpp)
target_include_directories(rmx_probe PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/Source"
"${CMAKE_CURRENT_BINARY_DIR}/Source"
"${CMAKE_CURRENT_BINARY_DIR}/juce_binarydata_RMX19_Assets/JuceLibraryCode")
target_link_libraries(rmx_probe PRIVATE
RMX19_Assets
juce::juce_audio_utils
juce::juce_gui_basics
juce::juce_dsp)

View file

@ -17,6 +17,10 @@ CMAKE := cmake
BUILD_DIR_ABSPATH := $(abspath $(BUILD_DIR))
ARTEFACTS := $(BUILD_DIR)/RMX19_artefacts/$(CONFIG)
JUCE_DIR := $(BUILD_DIR)/JUCE
JUCE_REPO := https://github.com/juce-framework/JUCE.git
JUCE_TAG := 8.0.4
VST3_SRC := $(ARTEFACTS)/VST3/$(PLUGIN_BASE).vst3
AU_SRC := $(ARTEFACTS)/AU/$(PLUGIN_BASE).component
@ -28,17 +32,25 @@ USER_PLUGIN_DIRS := $(HOME)/Library/Audio/Plug-Ins/VST3 $(HOME)/Library/Audio/Pl
.EXPORT_ALL_VARIABLES:
CMAKE_BUILD_TYPE := $(CONFIG)
.PHONY: all configure build install uninstall clean info
.PHONY: all configure build install uninstall clean info juce
all: build
build: configure
$(CMAKE) --build $(BUILD_DIR) --config $(CONFIG) --parallel
juce:
@if [ ! -d "$(JUCE_DIR)" ]; then \
echo "Fetching JUCE $(JUCE_TAG)..."; \
git clone --branch $(JUCE_TAG) --depth 1 $(JUCE_REPO) $(JUCE_DIR); \
else \
echo "JUCE already present at $(JUCE_DIR)"; \
fi
# Re-run cmake on every invocation: it is cheap when nothing changed and keeps
# the footer's git branch/hash and local build timestamp fresh.
configure:
$(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(CONFIG)
configure: juce
$(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(CONFIG) -DJUCE_ROOT=$(JUCE_DIR)
install: build
@codesign --force --deep --sign - "$(VST3_SRC)" >/dev/null 2>&1 || true

269
README.md
View file

@ -0,0 +1,269 @@
# RMX-19 — 19" Rack Digital Sound Module (VST3 / AU)
RMX-19 is a virtual analog synthesizer implemented as an audio plugin. It is
styled as a piece of 19-inch studio rack hardware from the late 1980s / early
1990s, complete with a working dot-matrix LCD, a metal faceplate, nav-diamond
buttons, and a green-on-black display — but underneath it is a fully modern
JUCE-based software instrument that runs inside any DAW that hosts AU or VST3
plugins on macOS (Apple Silicon).
The plugin is a self-contained subtractive synth: two oscillators, a
state-variable filter, dual envelopes, an LFO, a built-in FX unit (chorus /
delay / space), and a factory patch bank — all edited and browsed through the
on-screen LCD instead of a conventional panel of knobs.
> Credit where it's due: RMX-19 is an homage to the hardware-style UI of
> instruments like the Novation **Bass Station Rack** — hence the "RACK SYNTH"
> branding and the LCD-plus-soft-button interaction model.
---
## What it is at a glance
| | |
|---|---|
| **Format** | VST3 and Audio Unit (AU) plugin |
| **Transport** | Host JUCE-based AU / VST3 on macOS arm64 |
| **Architecture** | Subtractive / virtual-analog synthesizer |
| **UI model** | Simulated 19" rack module with a 160×64 dot-matrix LCD |
| **Voice count** | 16-voice polyphonic engine (1–16 selectable) |
| **Audio** | 24-bit/96 kHz capable; optional 2× HQ oversampling |
| **MIDI** | Note on/off, aftertouch-style CCs, pitch wheel, sustain pedal |
---
## Feature overview
### Sound engine
- **Two oscillators** per voice, each with five waveforms:
sine, triangle, sawtooth, pulse, and noise.
- **Oscillator mix** (blend between OSC1 and OSC2) and **detune** (in cents)
for rich, thick unison-style stacking.
- **State-variable filter** in the Chamberlin style with three modes:
low-pass, band-pass, and high-pass, plus **cutoff**, **resonance**,
**filter envelope amount**, and **key tracking**.
- **Two ADSR envelopes**: one for the amplifier and one for the filter,
each with independent attack / decay / sustain / release.
- **One LFO** with rate, depth, and four selectable shapes
(sine, triangle, square, sample-and-hold), routable to three targets:
filter cutoff, pitch, or amplitude (tremolo).
- **Portamento / glide** per patch, **master tune** (±12 semitones).
- **Voice panning**: user pan plus a small per-voice random offset to spread
the stereo image.
### Effects
The FX unit is stacked **chorus** and **delay**, selectable via **FX TYPE**:
- **OFF** — bypass.
- **CHORUS** — modulated short delay (uses the FX time as a base).
- **DELAY** — ping-pong-style feedback delay with a cross-feedback network.
- **SPACE** — a combination of chorus + delay (widened stereo ambience).
**FX DEPTH** sets the wet/dry mix; **FX TIME** sets the delay time in seconds
when running free. Crucially, the delay can also be **quantized to the host
tempo** (see below).
#### Delay tempo sync
The delay can be locked to your DAW's transport tempo. On the **FX** screen
there is a fourth row labeled **SYNC** that chooses from these rhythmic
divisions (values are multiplied against `60 / BPM` to derive the delay in
seconds):
| SYNC value | Music-notation equivalent |
|------------|---------------------------|
| `OFF` | free (use FX TIME in seconds) |
| `1/16` | sixteenth note |
| `1/8` | eighth note |
| `1/8T` | eighth-note triplet |
| `1/8D` | dotted eighth |
| `1/4` | quarter note |
| `1/4T` | quarter-note triplet |
| `1/4D` | dotted quarter |
| `1/2` | half note |
| `3/4` | dotted half |
| `1/1` | whole note |
When a SYNC value other than `OFF` is selected, the FX TIME knob is ignored
for the delay and the delay time follows the host tempo in real time.
### Factory patches
RMX-19 ships with a hand-tuned bank of factory presets organized by category
(POLY PAD, KEY, BASS, LEAD, and more). Patches are stored as parameter
settings and can be recalled either from the LCD patch browser or from the
DAW's own program list.
---
## The rack front panel (UI)
The plugin looks like a 19" rack unit sitting in your DAW. The main elements:
- **LCD display** — the green-on-black dot-matrix screen where all editing
happens.
- **Nav diamond** (`▲ ▼ ◀ ▶`) — move the cursor / change values.
- **ENTER** and **BACK** — confirm a selection or return to the previous
screen.
- **UI scale** dropdown (bottom-right) — resize the whole rack anywhere from
50% to 300%.
- A **power LED** and cosmetic backplate graphics round out the hardware
illusion.
You can also drive the whole interface with the keyboard: **arrow keys** for
navigation, **Return / Space** for ENTER, and **Escape** for BACK.
### Using the LCD
The interface is a small nested menu system:
1. **SPLASH** — the boot-up screen; press ENTER to continue.
2. **HOME** — shows the current patch number, name, category, and a live
voice-activity bar. Use ◀/▶ (or ▲/▼) to step through patches.
3. **MENU** — eight destinations:
`PATCH`, `VOICE`, `FILTER`, `ENVELOPE`, `LFO`, `FX`, `MASTER`, `ABOUT`.
4. **PATCH** — scroll through the factory bank; press ENTER to load the
highlighted patch.
5. **VOICE / FILTER / ENVELOPE / LFO / FX / MASTER** — parameter screens.
Highlight a row and use ◀/▶ to adjust it (defaults to fine steps).
#### The parameter screens
| Screen | Parameters |
|--------|------------|
| **VOICE** | OSC1 WAVE, OSC2 WAVE, DETUNE, OSC BAL |
| **FILTER** | FILTER TYPE, CUTOFF, RESONANCE, FILT ENV, KEY TRACK |
| **ENVELOPE** | AMP ATTACK, AMP DECAY, AMP SUSTAIN, AMP RELEASE, FENV ATTACK, FENV DECAY, FENV SUSTAIN, FENV RELEASE |
| **LFO** | LFO RATE, LFO SHAPE, LFO DEPTH, LFO TARGET |
| **FX** | FX TYPE, FX DEPTH, FX TIME, **SYNC** (delay tempo sync) |
| **MASTER** | MASTER VOL, STEREO PAN, POLYPHONY, PORTAMENTO, MASTER TUNE, HQ MODE |
> **Finding the delay sync.** On the **FX** screen the rows are FX TYPE,
> FX DEPTH, FX TIME, and then a fourth row labeled **SYNC**. Press **DOWN**
> three times from the top of the FX screen to reach it, then use **◀/▶** to
> pick a rhythmic division (see the table above).
### About screen
Shows the RMX-19 build: git branch / SHA / dirty marker, the build date/time,
the JUCE version, and the target platform. The LCD **footer** also shows the
git branch@commit and build timestamp on every screen.
---
## MIDI
RMX-19 accepts MIDI input and maps the following messages:
| MIDI | Action |
|------|--------|
| Note on / off | Play / release voices |
| Pitch wheel | ±2 semitones, smoothed |
| CC 64 | Sustain pedal (sustains held notes) |
| CC 1 / 74 | Filter cutoff |
| CC 2 / 71 | Filter resonance |
| CC 3 / 76 | LFO rate |
| CC 4 / 91 | FX depth |
| CC 7 | Master volume |
| CC 10 | Stereo pan |
| CC 72 | Amp release |
| CC 73 | Amp attack |
| CC 77 | LFO depth |
The plugin also exposes each patch as a DAW program, so preset changes from
the host map onto the factory bank.
---
## DSP notes
- **Voice engine** — each voice is a classic two-osc → mix → SVF filter →
amp-envelope chain. The oscillator mix is a crossfade between OSC1 and
OSC2, and detune offsets OSC2's frequency (OSC1 × `2^(detune/1200)`).
- **Filter** — a stable Chamberlin state-variable filter; cutoff is modulated
by the filter envelope, key tracking, the LFO, and the mod wheel, with a
smoothed control signal to suppress zipper noise.
- **Envelopes** — exponential ADSRs (attack/decay/release are time-domain
tau-based approximations, sustain is a level).
- **HQ mode** — a MASTER-screen toggle that renders the engine at **2× the
host sample rate** internally, then downsamples, reducing alias artifacts
at high cutoff / bright oscillators.
- **Output stage** — per-sample `tanh` soft-clipping plus a DC-blocking
filter, and a master volume gain.
- **FX** — a modulated short delay (chorus) and a cross-feedback delay line
(delay). The feedback delay supports host-tempo sync via the SYNC setting.
Tail length is sized to fit the longest possible sync delay.
---
## Building from source
### Prerequisites
- macOS on Apple Silicon (arm64), macOS 12.0 or later
- CMake ≥ 3.22
- A C++17 compiler (Apple Clang via Xcode / Command Line Tools)
- `git` (to fetch the JUCE framework)
The build fetches **JUCE 8.0.4** automatically into `build/JUCE` on first
configure — no manual JUCE setup is required.
### Commands
```sh
make # configure + build VST3 and AU (Release, arm64)
make install # build, ad-hoc codesign, and install into ~/Library/Audio/Plug-Ins
make uninstall # remove installed bundles
make clean # delete the build tree (and the fetched JUCE)
make info # print build paths
```
`make` (configure) is designed to be cheap when nothing changed and refreshes
the git branch/hash and timestamp embedded in the LCD footer on every
configure, so the About screen always reflects the current source state.
### Notes
- After `make install`, **restart (or cold-start) your DAW** so macOS rescans
AudioComponents. A freshly-installed AU may not appear until the host is
relaunched.
- Installed bundles are ad-hoc code-signed; if your DAW refuses to load the
AU, run `codesign --force --deep --sign - <bundle>` again or re-install.
---
## Project layout
```
CMakeLists.txt Build configuration (JUCE plugin target, git build info)
Makefile Building / installing convenience wrapper
b2.png Rack faceplate background image (embedded as binary data)
Source/
PluginProcessor.cpp/.h Audio processor: MIDI, FX engine, patch I/O, state save/load
PluginEditor.cpp/.h Rack front panel: buttons, scale dropdown, rendering
LcdPanel.cpp/.h Dot-matrix LCD: all screens, navigation, glyph rendering
SynthEngine.cpp/.h The voice / oscillator / filter / envelope DSP core
Params.cpp/.h Parameter table, formatting, UI value stepping, delay sync names
PresetBank.cpp/.h Factory patch definitions and bank lookup
Probe.cpp Offline dev probe (scripted MIDI, no GUI/DAW)
BuildInfo.h.in Compile-time git/timestamp template
```
### Development probe
`Source/Probe.cpp` is an offline executable (`rmx_probe`) used during
development to drive the processor with scripted MIDI events without a DAW or
GUI. It is defined in `CMakeLists.txt` for convenience and is not part of the
shipped plugin.
---
## License / attribution
RMX-19 is a personal / hobby project. It is built on the
[JUCE](https://juce.com) audio framework under its own license. The rack
hardware aesthetic pays tribute to classic 90s synthesizer rack gear; the
plugin is an original implementation and is not affiliated with or endorsed
by any hardware manufacturer.

View file

@ -1,5 +1,6 @@
#include "LcdPanel.h"
#include "BuildInfo.h"
#include <cstring>
// ---------------------------------------------------------------------------
// 5x7 dot-matrix font. Each glyph is 5 columns x 7 rows, bit 6 (0x40) of each
@ -78,6 +79,94 @@ static const Entry table[] =
} // namespace
// ---------------------------------------------------------------------------
// 3x5 compact font (denser layout, reads like a small real-world LCD). Each
// glyph is 3 columns x 5 rows, bits 0..2 (bit 2 is the leftmost dot).
// ---------------------------------------------------------------------------
namespace {
static constexpr int MW = 3, MH = 5;
struct MEntry { int key; const juce::uint8* rows; };
static const juce::uint8 mA_[MH] = { 0b010, 0b101, 0b111, 0b101, 0b101 };
static const juce::uint8 mB_[MH] = { 0b110, 0b101, 0b110, 0b101, 0b110 };
static const juce::uint8 mC_[MH] = { 0b011, 0b100, 0b100, 0b100, 0b011 };
static const juce::uint8 mD_[MH] = { 0b110, 0b101, 0b101, 0b101, 0b110 };
static const juce::uint8 mE_[MH] = { 0b111, 0b100, 0b110, 0b100, 0b111 };
static const juce::uint8 mF_[MH] = { 0b111, 0b100, 0b110, 0b100, 0b100 };
static const juce::uint8 mG_[MH] = { 0b011, 0b100, 0b101, 0b101, 0b011 };
static const juce::uint8 mH_[MH] = { 0b101, 0b101, 0b111, 0b101, 0b101 };
static const juce::uint8 mI_[MH] = { 0b111, 0b010, 0b010, 0b010, 0b111 };
static const juce::uint8 mJ_[MH] = { 0b111, 0b001, 0b001, 0b101, 0b010 };
static const juce::uint8 mK_[MH] = { 0b101, 0b110, 0b100, 0b110, 0b101 };
static const juce::uint8 mL_[MH] = { 0b100, 0b100, 0b100, 0b100, 0b111 };
static const juce::uint8 mM_[MH] = { 0b101, 0b111, 0b111, 0b101, 0b101 };
static const juce::uint8 mN_[MH] = { 0b110, 0b101, 0b101, 0b101, 0b101 };
static const juce::uint8 mO_[MH] = { 0b010, 0b101, 0b101, 0b101, 0b010 };
static const juce::uint8 mP_[MH] = { 0b110, 0b101, 0b110, 0b100, 0b100 };
static const juce::uint8 mQ_[MH] = { 0b010, 0b101, 0b101, 0b110, 0b011 };
static const juce::uint8 mR_[MH] = { 0b110, 0b101, 0b110, 0b110, 0b101 };
static const juce::uint8 mS_[MH] = { 0b011, 0b100, 0b010, 0b001, 0b110 };
static const juce::uint8 mT_[MH] = { 0b111, 0b010, 0b010, 0b010, 0b010 };
static const juce::uint8 mU_[MH] = { 0b101, 0b101, 0b101, 0b101, 0b111 };
static const juce::uint8 mV_[MH] = { 0b101, 0b101, 0b101, 0b101, 0b010 };
static const juce::uint8 mW_[MH] = { 0b101, 0b111, 0b101, 0b111, 0b101 };
static const juce::uint8 mX_[MH] = { 0b101, 0b101, 0b010, 0b101, 0b101 };
static const juce::uint8 mY_[MH] = { 0b101, 0b101, 0b010, 0b010, 0b010 };
static const juce::uint8 mZ_[MH] = { 0b111, 0b001, 0b010, 0b100, 0b111 };
static const juce::uint8 mn0_[MH] = { 0b010, 0b101, 0b101, 0b101, 0b010 };
static const juce::uint8 mn1_[MH] = { 0b011, 0b001, 0b001, 0b001, 0b011 };
static const juce::uint8 mn2_[MH] = { 0b110, 0b001, 0b010, 0b100, 0b111 };
static const juce::uint8 mn3_[MH] = { 0b110, 0b001, 0b110, 0b001, 0b110 };
static const juce::uint8 mn4_[MH] = { 0b101, 0b101, 0b111, 0b001, 0b001 };
static const juce::uint8 mn5_[MH] = { 0b111, 0b100, 0b110, 0b001, 0b110 };
static const juce::uint8 mn6_[MH] = { 0b010, 0b100, 0b110, 0b101, 0b010 };
static const juce::uint8 mn7_[MH] = { 0b111, 0b001, 0b001, 0b010, 0b010 };
static const juce::uint8 mn8_[MH] = { 0b010, 0b101, 0b010, 0b101, 0b010 };
static const juce::uint8 mn9_[MH] = { 0b010, 0b101, 0b011, 0b001, 0b010 };
static const juce::uint8 msp_[MH] = { 0, 0, 0, 0, 0 };
static const juce::uint8 mdt_[MH] = { 0, 0, 0, 0b011, 0b011 };
static const juce::uint8 mco_[MH] = { 0, 0, 0, 0b110, 0b100 };
static const juce::uint8 mcl_[MH] = { 0, 0b010, 0, 0b010, 0 };
static const juce::uint8 mmi_[MH] = { 0, 0, 0b111, 0, 0 };
static const juce::uint8 mpl_[MH] = { 0, 0b010, 0b111, 0b010, 0 };
static const juce::uint8 msl_[MH] = { 0b001, 0b001, 0b010, 0b100, 0b100 };
static const juce::uint8 mpc_[MH] = { 0b101, 0b001, 0b010, 0b100, 0b101 };
static const juce::uint8 map_[MH] = { 0b010, 0b010, 0, 0, 0 };
static const juce::uint8 mlt_[MH] = { 0b100, 0b010, 0b001, 0b010, 0b100 };
static const juce::uint8 mgt_[MH] = { 0b001, 0b010, 0b100, 0b010, 0b001 };
static const juce::uint8 meq_[MH] = { 0, 0b111, 0, 0b111, 0 };
static const juce::uint8 mhs_[MH] = { 0b010, 0b111, 0b111, 0b111, 0b010 };
static const juce::uint8 mst_[MH] = { 0, 0, 0, 0, 0b111 };
static const juce::uint8 mex_[MH] = { 0b010, 0b010, 0b010, 0, 0b010 };
static const juce::uint8 mas_[MH] = { 0, 0b101, 0b010, 0b101, 0 };
static const MEntry mtable[] =
{
{ 'A', mA_ }, { 'B', mB_ }, { 'C', mC_ }, { 'D', mD_ }, { 'E', mE_ },
{ 'F', mF_ }, { 'G', mG_ }, { 'H', mH_ }, { 'I', mI_ }, { 'J', mJ_ },
{ 'K', mK_ }, { 'L', mL_ }, { 'M', mM_ }, { 'N', mN_ }, { 'O', mO_ },
{ 'P', mP_ }, { 'Q', mQ_ }, { 'R', mR_ }, { 'S', mS_ }, { 'T', mT_ },
{ 'U', mU_ }, { 'V', mV_ }, { 'W', mW_ }, { 'X', mX_ }, { 'Y', mY_ },
{ 'Z', mZ_ }, { '0', mn0_ }, { '1', mn1_ }, { '2', mn2_ }, { '3', mn3_ },
{ '4', mn4_ }, { '5', mn5_ }, { '6', mn6_ }, { '7', mn7_ }, { '8', mn8_ },
{ '9', mn9_ }, { ' ', msp_ }, { '.', mdt_ }, { ',', mco_ }, { ':', mcl_ },
{ '-', mmi_ }, { '+', mpl_ }, { '/', msl_ }, { '%', mpc_ }, { '\'', map_ },
{ '<', mlt_ }, { '>', mgt_ }, { '=', meq_ }, { '#', mhs_ }, { '_', mst_ },
{ '!', mex_ }, { '*', mas_ },
};
} // namespace
const juce::uint8* LcdPanel::glyphM (int key)
{
if (key >= 'a' && key <= 'z') key = key - 'a' + 'A';
for (const auto& e : mtable)
if (e.key == key) return e.rows;
return msp_;
}
const juce::uint8* LcdPanel::glyphFor (int key)
{
if (key >= 'a' && key <= 'z') key = key - 'a' + 'A';
@ -91,7 +180,10 @@ const juce::uint8* LcdPanel::glyphFor (int key)
LcdPanel::LcdPanel (RMX19AudioProcessor& p)
: proc (p)
{
dotImage = juce::Image (juce::Image::ARGB, gridW, gridH, true);
dotImage = juce::Image (juce::Image::ARGB, gridW, gridH, false);
displayImage = juce::Image (juce::Image::ARGB, gridW * 2, gridH * 2, false);
backdrop = juce::Image (juce::Image::ARGB, gridW * 2, gridH * 2, false);
buildBackdrop();
splashUntil = juce::Time::getMillisecondCounter() + 1500;
lastMidi = proc.getLastMidiTime();
startTimerHz (22);
@ -104,20 +196,35 @@ LcdPanel::~LcdPanel()
void LcdPanel::timerCallback()
{
float l, r; proc.getMeters (l, r);
meterL = std::max (l, meterL * 0.5f);
meterR = std::max (r, meterR * 0.5f);
bool needsRedraw = (screen == Screen::Splash);
if (proc.getLastMidiTime() != lastMidi)
{
lastMidi = proc.getLastMidiTime();
powerGlow = 1.0f;
needsRedraw = true;
}
powerGlow = std::max (0.0f, powerGlow - 0.04f);
const float oldGlow = powerGlow;
powerGlow = std::max (0.0f, powerGlow - 0.05f);
if (powerGlow >= 0.05f || oldGlow >= 0.05f) needsRedraw = true;
if (screen == Screen::Splash && juce::Time::getMillisecondCounter() > splashUntil)
{
screen = Screen::Home;
needsRedraw = true;
}
if (screen == Screen::Home)
{
const int v = proc.getActiveVoices();
if (v != lastVoices)
{
lastVoices = v;
needsRedraw = true;
}
}
if (needsRedraw)
setDirty();
}
@ -132,7 +239,6 @@ void LcdPanel::paint (juce::Graphics& g)
g.drawRect (b, 2);
// inner LCD glass (black)
const float scale = (float) b.getWidth() / (float) gridW;
const int cell = 2;
const int w = gridW * cell;
const int h = gridH * cell;
@ -141,26 +247,150 @@ void LcdPanel::paint (juce::Graphics& g)
g.setColour (juce::Colour (0xff020703));
g.fillRect (ox, oy, w, h);
if (contentDirty)
{
rebuild();
contentDirty = false;
}
g.drawImageAt (displayImage, ox, oy);
}
void LcdPanel::buildBackdrop()
{
const int W = displayImage.getWidth();
const int H = displayImage.getHeight();
juce::Graphics dg (backdrop);
// dark glass base
dg.fillAll (juce::Colour (0xff020703));
// even, faint backlight (brightest toward the middle of the glass)
{
const float cx = W * 0.5f, cy = H * 0.5f;
juce::ColourGradient backlight (juce::Colour::fromRGBA (0x28, 0x5a, 0x3c, 22), cx, cy,
juce::Colour::fromRGBA (0x18, 0x2e, 0x20, 0), W * 0.38f, cy, false);
dg.setGradientFill (backlight);
dg.fillRect (backdrop.getBounds());
}
// glass reflection sheen across the top
{
juce::ColourGradient sheen (juce::Colour::fromRGBA (0xff, 0xff, 0xff, 16), 0.0f, 2.0f,
juce::Colour::fromRGBA (0xff, 0xff, 0xff, 0), 0.0f, 42.0f, false);
dg.setGradientFill (sheen);
dg.fillRect (0, 0, W, 42);
}
// subtle vignette so the glass reads as a luminous surface
{
juce::ColourGradient eT (juce::Colour::fromRGBA (0x00, 0x08, 0x04, 60), 0, 0,
juce::Colour::fromRGBA (0x00, 0x08, 0x04, 0), 0, 10, false);
dg.setGradientFill (eT); dg.fillRect (0, 0, W, 10);
juce::ColourGradient eB (juce::Colour::fromRGBA (0x00, 0x08, 0x04, 60), 0, (float) H,
juce::Colour::fromRGBA (0x00, 0x08, 0x04, 0), 0, (float) H - 10, false);
dg.setGradientFill (eB); dg.fillRect (0, H - 10, W, 10);
juce::ColourGradient eL (juce::Colour::fromRGBA (0x00, 0x08, 0x04, 36), 0, 0,
juce::Colour::fromRGBA (0x00, 0x08, 0x04, 0), 10, 0, false);
dg.setGradientFill (eL); dg.fillRect (0, 0, 10, H);
juce::ColourGradient eR (juce::Colour::fromRGBA (0x00, 0x08, 0x04, 36), (float) W, 0,
juce::Colour::fromRGBA (0x00, 0x08, 0x04, 0), (float) W - 10, 0, false);
dg.setGradientFill (eR); dg.fillRect (W - 10, 0, 10, H);
}
// faint "ghost" dot for every (unlit) segment, exactly like a passive matrix LCD
for (int y = 0; y < gridH; ++y)
for (int x = 0; x < gridW; ++x)
{
const float cx = x * 2.0f + 1.0f;
const float cy = y * 2.0f + 1.0f;
dg.setColour (juce::Colour::fromRGBA (0x00, 0x88, 0x55, 6));
dg.fillEllipse (cx - 0.55f, cy - 0.55f, 1.1f, 1.1f);
}
}
void LcdPanel::rebuild()
{
// Image::clear() hard-zeros pixels; Graphics::fillAll(transparent black) only blends
// and leaves stale content behind.
dotImage.clear (dotImage.getBounds());
{
juce::Graphics dg (dotImage);
dg.fillAll (juce::Colours::black);
drawContent (dg);
}
for (int y = 0; y < gridH; ++y)
const int W = displayImage.getWidth();
const int H = displayImage.getHeight();
// start from the pre-rendered base layer (backlight, sheen, vignette, ghost dots)
{
for (int x = 0; x < gridW; ++x)
juce::Image::BitmapData src (backdrop, juce::Image::BitmapData::readOnly);
juce::Image::BitmapData dst (displayImage, juce::Image::BitmapData::writeOnly);
for (int y = 0; y < H; ++y)
std::memcpy (dst.getLinePointer (y), src.getLinePointer (y), (size_t) W * 4u);
}
// blend the lit segments in as crisp round dots with just a hint of bloom.
// The core is near-solid so glyphs stay sharp; the glow is faint and tight
// so it can't wash the dark gaps between rows/columns.
constexpr float coreR2 = 0.36f; // bright core radius ~0.60 cell
constexpr float glowR2 = 1.10f; // faint bloom radius ~1.05 cells
{
const auto px = dotImage.getPixelAt (x, y);
const float lvl = px.getAlpha() / 255.0f;
if (lvl <= 0.0f) continue;
const juce::Colour c = juce::Colour::fromFloatRGBA (0.04f + 0.02f * powerGlow,
0.96f * lvl, 0.25f * lvl, 1.0f);
g.setColour (c);
g.fillRect (ox + x * cell, oy + y * cell, cell, cell);
juce::Image::BitmapData cells (dotImage, juce::Image::BitmapData::readOnly);
juce::Image::BitmapData out (displayImage, juce::Image::BitmapData::readWrite);
for (int cy = 0; cy < gridH; ++cy)
{
for (int cx = 0; cx < gridW; ++cx)
{
const float lvl = cells.getPixelColour (cx, cy).getAlpha() / 255.0f;
if (lvl <= 0.02f) continue;
const float ccx = cx * 2.0f + 1.0f;
const float ccy = cy * 2.0f + 1.0f;
const int x0 = cx * 2 - 1, x1 = cx * 2 + 3;
const int y0 = cy * 2 - 1, y1 = cy * 2 + 3;
for (int py = y0; py <= y1; ++py)
{
if (py < 0 || py >= H) continue;
for (int px = x0; px <= x1; ++px)
{
if (px < 0 || px >= W) continue;
const float dx = (px - ccx) * 0.5f;
const float dy = (py - ccy) * 0.5f;
const float d2 = dx * dx + dy * dy;
if (d2 > glowR2) continue;
float a;
juce::uint8 r, g, b;
if (d2 <= coreR2)
{
a = (0.85f + 0.15f * (1.0f - d2 / coreR2)) * lvl;
r = 0xdf; g = 0xff; b = 0xe6;
}
else
{
a = (1.0f - (d2 - coreR2) / (glowR2 - coreR2)) * 0.16f * lvl;
r = 0x90; g = 0xff; b = 0xa8;
}
if (a <= 0.002f) continue;
const juce::Colour base = out.getPixelColour (px, py);
const float ia = 1.0f - a;
out.setPixelColour (px, py, juce::Colour (
(juce::uint8) juce::jmin (255.0f, (float) base.getRed() * ia + r * a),
(juce::uint8) juce::jmin (255.0f, (float) base.getGreen() * ia + g * a),
(juce::uint8) juce::jmin (255.0f, (float) base.getBlue() * ia + b * a)));
}
}
}
}
}
(void) scale;
}
void LcdPanel::drawContent (juce::Graphics& g)
@ -175,7 +405,6 @@ void LcdPanel::drawContent (juce::Graphics& g)
case Screen::System: drawSystem (g); break;
case Screen::Info: drawInfo (g); break;
}
drawFooter (g);
}
void LcdPanel::px (juce::Graphics& g, int x, int y, float intensity)
@ -205,6 +434,24 @@ void LcdPanel::pxText (juce::Graphics& g, int x, int y, const juce::String& text
}
}
void LcdPanel::pxTextM (juce::Graphics& g, int x, int y, const juce::String& text, float intensity)
{
int cx = x;
for (int i = 0; i < text.length(); ++i)
{
const int key = text[i];
const juce::uint8* rows = glyphM (key);
for (int row = 0; row < MH; ++row)
{
const juce::uint8 bits = rows[row];
for (int col = 0; col < MW; ++col)
if (bits & (1u << (MW - 1 - col)))
px (g, cx + col, y + row, intensity);
}
cx += MW + 1;
}
}
void LcdPanel::drawFilled (juce::Graphics& g, int x, int y, int w, int h, float intensity)
{
for (int yy = 0; yy < h; ++yy)
@ -212,6 +459,12 @@ void LcdPanel::drawFilled (juce::Graphics& g, int x, int y, int w, int h, float
px (g, x + xx, y + yy, intensity);
}
// full-width selection bar: lit row behind a dimmed-to-transparent glyph
void LcdPanel::drawSelBar (juce::Graphics& g, int y)
{
drawFilled (g, 2, y, gridW - 4, 6, 0.55f);
}
void LcdPanel::drawHBar (juce::Graphics& g, int x, int y, int w, double level, float intensity)
{
const int filled = juce::roundToInt (w * juce::jlimit (0.0, 1.0, level));
@ -223,25 +476,22 @@ void LcdPanel::drawFooter (juce::Graphics& g)
juce::String branch = juce::String (BUILD_GIT_BRANCH);
if (branch.length() > 6) branch = branch.substring (0, 6);
const juce::String git = branch + "@" + juce::String (BUILD_GIT_COMMIT) + juce::String (BUILD_GIT_DIRTY);
const juce::String dt = juce::String (BUILD_DATE).substring (5) + " " + juce::String (BUILD_TIME);
drawFilled (g, 0, 61, gridW, 1, 0.4f);
pxText (g, 2, 63, git, 0.75f);
drawFilled (g, 2, 57, gridW - 4, 1, 0.35f);
pxTextM (g, 4, 58, git, 0.75f);
const auto w = dt.length() * (NW + 1) - 1;
pxText (g, gridW - w - 2, 63, dt, 0.75f);
(void) powerGlow;
const juce::String dt = juce::String (BUILD_DATE) + " " + juce::String (BUILD_TIME);
const auto w = dt.length() * (MW + 1) - 1;
pxTextM (g, gridW - w - 4, 58, dt, 0.75f);
}
void LcdPanel::drawSplash (juce::Graphics& g)
{
pxText (g, 46, 6, "* RMX-19 *", 1.0f);
pxText (g, 30, 18, "RACK SYNTHESIZER", 0.9f);
drawFilled (g, 10, 30, gridW - 20, 1, 0.5f);
pxText (g, 44, 34, "INITIALIZING", 0.8f);
pxText (g, 34, 45, "DSP READY", 0.75f);
pxText (g, 56, 56, "REV.B", 0.6f);
pxTextM (g, 58, 8, "* RMX-19 *", 1.0f);
pxTextM (g, 28, 20, "RACK SYNTHESIZER", 0.9f);
drawFilled (g, 10, 31, gridW - 20, 1, 0.5f);
pxTextM (g, 44, 36, "INITIALIZING", 0.8f);
pxTextM (g, 34, 46, "DSP READY", 0.75f);
}
void LcdPanel::drawHome (juce::Graphics& g)
@ -251,86 +501,59 @@ void LcdPanel::drawHome (juce::Graphics& g)
const juce::String num = juce::String (patch + 1).paddedLeft ('0', 2);
pxText (g, 2, 3, num, 1.0f);
pxText (g, 4, 15, p.name, 1.0f);
pxText (g, 4, 28, p.category, 0.8f);
pxText (g, 14, 3, p.name, 1.0f);
pxTextM (g, 4, 17, p.category, 0.9f);
drawFilled (g, 4, 42, 88, 1, 0.5f);
drawHBar (g, 4, 43, 88, proc.getActiveVoices() / 16.0, 0.9f);
pxText (g, 116, 42, "L", 0.6f);
pxText (g, 138, 42, "R", 0.6f);
drawFilled (g, 122, 40, 1, 8, 0.5f);
drawHBar (g, 123, 41, 12, meterL, 1.0f);
drawHBar (g, 123, 44, 12, meterR, 1.0f);
drawFilled (g, 123, 40, 12, 1, 0.4f);
drawFilled (g, 123, 47, 12, 1, 0.4f);
drawFilled (g, 4, 28, 88, 1, 0.5f);
drawHBar (g, 4, 29, 88, proc.getActiveVoices() / 16.0, 0.9f);
}
void LcdPanel::drawMenu (juce::Graphics& g)
{
static const char* items[] = {
"PATCH SELECT", "OSC SELECT", "FILTER SETUP", "AMP ENVELOPE",
"FILT ENVELOPE", "MODULATION", "EFFECTS", "MASTER", "SYSTEM", "ABOUT"
"PATCH", "VOICE", "FILTER", "ENVELOPE", "LFO", "FX", "MASTER", "ABOUT"
};
const int rows = (int) juce::numElementsInArray (items);
const int visible = 6;
const int top = juce::jmax (0, juce::jmin (cursor - 3, rows - visible));
for (int i = 0; i < visible; ++i)
for (int i = 0; i < rows; ++i)
{
const int idx = top + i;
if (idx >= rows) break;
const int y = 2 + i * 10 + 1;
const bool sel = (idx == cursor);
pxText (g, 4, y, items[idx], sel ? 1.0f : 0.6f);
if (sel)
{
drawFilled (g, 1, y, 1, NH, 1.0f);
pxText (g, 142, y, ">", 1.0f);
const int y = 2 + i * 7;
const bool sel = (i == cursor);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, items[i], sel ? 1.0f : 0.72f);
}
}
drawFilled (g, 151, 8, 1, 48, 0.4f);
const double frac = rows > 1 ? cursor / (double) (rows - 1) : 0.0;
const int thumbY = 8 + (int) (frac * 40.0);
drawFilled (g, 150, thumbY - 2, 3, 4, 0.8f);
pxText (g, 4, 62, "UP/DN MOVE OK OPEN", 0.55f);
}
void LcdPanel::drawPatchList (juce::Graphics& g)
{
const int bankSize = proc.getBank().size();
const int visible = 6;
const int top = juce::jmax (0, juce::jmin (patchCursor - 2, bankSize - visible));
const int visible = 8;
const int top = juce::jmax (0, juce::jmin (patchCursor - 3, bankSize - visible));
for (int i = 0; i < visible; ++i)
{
const int idx = top + i;
if (idx >= bankSize) break;
const auto& p = proc.getBank().get (idx);
const int y = 2 + i * 10 + 1;
const int y = 2 + i * 7;
const bool sel = (idx == patchCursor);
pxText (g, 4, y, juce::String (idx + 1).paddedLeft ('0', 2) + " " + juce::String (p.name),
sel ? 1.0f : 0.6f);
if (sel) drawFilled (g, 1, y, 1, NH, 1.0f);
if (idx == proc.getPatchIndex()) pxText (g, 138, y, "*", 0.9f);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, juce::String (idx + 1).paddedLeft ('0', 2) + " " + juce::String (p.name),
sel ? 1.0f : 0.72f);
if (idx == proc.getPatchIndex())
pxTextM (g, gridW - 7, y, "*", sel ? 1.0f : 0.9f);
}
drawFilled (g, 140, 8, 1, 48, 0.4f);
const double frac = bankSize > 1 ? patchCursor / (double) (bankSize - 1) : 0.0;
const int thumbY = 8 + (int) (frac * 40.0);
drawFilled (g, 139, thumbY - 2, 3, 4, 0.8f);
pxText (g, 4, 62, "UP/DN BROWSE OK LOAD", 0.55f);
}
void LcdPanel::drawParamScreen (juce::Graphics& g)
{
pxText (g, 2, 2, paramScreenTitle (paramScreenIdx), 1.0f);
drawFilled (g, 2, 9, gridW - 4, 1, 0.35f);
pxTextM (g, 3, 1, paramScreenTitle (paramScreenIdx), 1.0f);
drawFilled (g, 3, 6, gridW - 6, 1, 0.35f);
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune, prm::Id::oscMix };
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
static const prm::Id ampEnv[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR };
static const prm::Id fltEnv[] = { prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id mod[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune, prm::Id::hq };
const prm::Id* rows = nullptr;
int count = 0;
@ -338,39 +561,47 @@ void LcdPanel::drawParamScreen (juce::Graphics& g)
{
case 0: rows = voice; count = (int) juce::numElementsInArray (voice); break;
case 1: rows = filter; count = (int) juce::numElementsInArray (filter); break;
case 2: rows = ampEnv; count = (int) juce::numElementsInArray (ampEnv); break;
case 3: rows = fltEnv; count = (int) juce::numElementsInArray (fltEnv); break;
case 4: rows = mod; count = (int) juce::numElementsInArray (mod); break;
case 5: rows = fx; count = (int) juce::numElementsInArray (fx); break;
case 6: rows = master; count = (int) juce::numElementsInArray (master); break;
case 2: rows = envMt; count = (int) juce::numElementsInArray (envMt); break;
case 3: rows = lfo; count = (int) juce::numElementsInArray (lfo); break;
case 4: rows = fx; count = (int) juce::numElementsInArray (fx); break;
case 5: rows = master; count = (int) juce::numElementsInArray (master); break;
default: return;
}
const int visible = 5;
const int top = juce::jmax (0, juce::jmin (cursor - 2, count - visible));
const int visible = 7;
const int top = juce::jmax (0, juce::jmin (cursor - 3, count - visible));
for (int i = 0; i < visible; ++i)
{
const int ri = top + i;
if (ri >= count) break;
const int y = 8 + i * 7;
const bool sel = (ri == cursor);
// FX screen has an extra "SYNC" (delay tempo sync) row after the 3 fx params
if (paramScreenIdx == 4 && ri >= (int) juce::numElementsInArray (fx))
{
const int syncIdx = (int) proc.apvts.getRawParameterValue ("delaysync")->load();
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, "SYNC", sel ? 1.0f : 0.75f);
const juce::String val = prm::delaySyncName (syncIdx);
const auto w = val.length() * (MW + 1) - 1;
pxTextM (g, gridW - w - 5, y, val, sel ? 1.0f : 0.85f);
continue;
}
const prm::Id id = rows[ri];
const auto& d = prm::desc (id);
const float cur = proc.apvts.getRawParameterValue (d.id)->load();
const auto* par = proc.apvts.getParameter (d.id);
const float v01 = par->convertTo0to1 (cur);
const int y = 12 + i * 10;
const bool sel = (ri == cursor);
pxText (g, 3, y, d.shortName, 0.55f);
pxText (g, 22, y, prm::formatParam (id, cur), sel ? 1.0f : 0.7f);
drawFilled (g, 22, y + 6, 130, 1, 0.15f);
drawHBar (g, 22, y + 6, 130, v01, sel ? 1.0f : 0.5f);
if (sel)
{
drawFilled (g, 0, y, 1, NH, 1.0f);
pxText (g, 148, y, "<>", 1.0f);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, d.name, sel ? 1.0f : 0.75f);
const juce::String val = prm::formatParam (id, cur);
const auto w = val.length() * (MW + 1) - 1;
pxTextM (g, gridW - w - 5, y, val, sel ? 1.0f : 0.85f);
}
}
pxText (g, 4, 62, "LEFT/RIGHT EDIT OK BACK", 0.55f);
}
void LcdPanel::drawSystem (juce::Graphics& g)
@ -378,22 +609,22 @@ void LcdPanel::drawSystem (juce::Graphics& g)
static const char* items[] = { "FACTORY INIT", "ALL NOTES OFF", "MIDI TEST", "BACK" };
for (int i = 0; i < 4; ++i)
{
const int y = 2 + i * 10 + 1;
const int y = 2 + i * 7;
const bool sel = (i == cursor);
pxText (g, 4, y, items[i], sel ? 1.0f : 0.6f);
if (sel) drawFilled (g, 1, y, 1, NH, 1.0f);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, items[i], sel ? 1.0f : 0.72f);
}
if (cursor == 2 && powerGlow > 0.5f)
pxText (g, 4, 50, "MIDI RX " + juce::String (lastMidi ? "ACTIVE" : "IDLE"), 0.6f);
pxTextM (g, 6, 50, "MIDI RX " + juce::String (lastMidi ? "ACTIVE" : "IDLE"), 0.6f);
}
void LcdPanel::drawInfo (juce::Graphics& g)
{
pxText (g, 2, 3, "RMX-19 RACK SYNTH", 0.9f);
pxText (g, 2, 15, "GIT " + juce::String (BUILD_GIT_BRANCH) + " " + BUILD_GIT_COMMIT + BUILD_GIT_DIRTY, 0.85f);
pxText (g, 2, 27, "BUILT " + juce::String (BUILD_DATE) + " " + BUILD_TIME, 0.85f);
pxText (g, 2, 39, "ARM64 JUCE VERSION " + juce::String (JUCE_MAJOR_VERSION) + "." + juce::String (JUCE_MINOR_VERSION), 0.7f);
pxText (g, 2, 51, "MIDI NOTE + CC CONTROL", 0.7f);
pxTextM (g, 3, 3, "RMX-19 RACK SYNTH BY A. JENEWEIN", 0.95f);
pxTextM (g, 3, 13, "GIT " + juce::String (BUILD_GIT_BRANCH) + " " + juce::String (BUILD_GIT_COMMIT) + BUILD_GIT_DIRTY, 0.85f);
pxTextM (g, 3, 23, "BUILT " + juce::String (BUILD_DATE) + " " + BUILD_TIME, 0.85f);
pxTextM (g, 3, 33, "ARM64 JUCE " + juce::String (JUCE_MAJOR_VERSION) + "." + juce::String (JUCE_MINOR_VERSION), 0.7f);
pxTextM (g, 3, 43, "MIDI NOTE + CC CONTROL", 0.7f);
}
// ---- editing helpers ------------------------------------------------------
@ -402,7 +633,7 @@ int LcdPanel::rowCountForScreen() const
{
switch (screen)
{
case Screen::Menu: return 10;
case Screen::Menu: return 8;
case Screen::System: return 4;
default: return 0;
}
@ -414,11 +645,10 @@ int LcdPanel::paramCountForScreen (int idx) const
{
case 0: return 4;
case 1: return 5;
case 2: return 4;
case 2: return 8;
case 3: return 4;
case 4: return 4;
case 5: return 3;
case 6: return 5;
case 5: return 5;
default: return 0;
}
}
@ -427,20 +657,19 @@ const char* LcdPanel::paramScreenTitle (int idx) const
{
switch (idx)
{
case 0: return "OSCILLATOR";
case 0: return "VOICE";
case 1: return "FILTER";
case 2: return "AMP ENVELOPE";
case 3: return "FILT ENVELOPE";
case 4: return "MODULATION";
case 5: return "EFFECTS";
case 6: return "MASTER";
case 2: return "ENVELOPE";
case 3: return "LFO";
case 4: return "FX";
case 5: return "MASTER";
default: return "";
}
}
void LcdPanel::openParamScreen (int idx)
{
paramScreenIdx = juce::jlimit (0, 6, idx);
paramScreenIdx = juce::jlimit (0, 5, idx);
cursor = 0;
enterScreen (Screen::Param);
}
@ -464,6 +693,17 @@ void LcdPanel::moveCursor (int d)
}
}
void LcdPanel::stepDelaySync (int dir)
{
auto* par = dynamic_cast<juce::AudioParameterInt*> (proc.apvts.getParameter ("delaysync"));
if (par == nullptr) return;
const int cur = (int) proc.apvts.getRawParameterValue ("delaysync")->load();
const int nv = juce::jlimit (0, prm::delaySyncCount - 1, cur + dir);
if (nv != cur)
par->setValueNotifyingHost (juce::NormalisableRange<double> (0.0, (double) (prm::delaySyncCount - 1)).convertTo0to1 ((double) nv));
setDirty();
}
void LcdPanel::stepParam (prm::Id id, int dir, bool coarse)
{
auto* par = dynamic_cast<juce::AudioParameterFloat*> (proc.apvts.getParameter (prm::desc (id).id));
@ -480,33 +720,56 @@ static prm::Id paramForCursor (int screenIdx, int row)
{
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune, prm::Id::oscMix };
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
static const prm::Id ampEnv[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR };
static const prm::Id fltEnv[] = { prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id mod[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune, prm::Id::hq };
switch (screenIdx)
{
case 0: return voice[row];
case 1: return filter[row];
case 2: return ampEnv[row];
case 3: return fltEnv[row];
case 4: return mod[row];
case 5: return fx[row];
case 6: return master[row];
case 2: return envMt[row];
case 3: return lfo[row];
case 4: return fx[row];
case 5: return master[row];
default: return prm::Id::vol;
}
}
void LcdPanel::adjustCurrent (int dir, bool coarse)
{
if (screen == Screen::Param)
if (screen != Screen::Param) return;
// FX screen: row 3 is the delay-sync selector (not a prm::Id)
if (paramScreenIdx == 4 && cursor >= 3)
{
stepDelaySync (dir);
return;
}
stepParam (paramForCursor (paramScreenIdx, cursor), dir, coarse);
}
void LcdPanel::navUp() { moveCursor (-1); }
void LcdPanel::navDown() { moveCursor (1); }
void LcdPanel::selectPatchCyclic (int dir)
{
const int n = proc.getBank().size();
if (n <= 0) return;
const int next = (proc.getPatchIndex() + dir + n) % n;
if (next != proc.getPatchIndex())
proc.selectPatch (next);
setDirty();
}
void LcdPanel::navUp() { if (screen == Screen::Home) { selectPatchCyclic (-1); return; } moveCursor (-1); applyPatchOnBrowserMove(); }
void LcdPanel::navDown() { if (screen == Screen::Home) { selectPatchCyclic (1); return; } moveCursor (1); applyPatchOnBrowserMove(); }
void LcdPanel::applyPatchOnBrowserMove()
{
if (screen == Screen::Patch)
{
proc.selectPatch (patchCursor);
setDirty();
}
}
void LcdPanel::navLeft() { adjustCurrent (-1, false); }
void LcdPanel::navRight() { adjustCurrent (1, false); }
@ -526,15 +789,13 @@ void LcdPanel::navEnter()
case 4: openParamScreen (3); break;
case 5: openParamScreen (4); break;
case 6: openParamScreen (5); break;
case 7: openParamScreen (6); break;
case 8: enterScreen (Screen::System); break;
case 9: enterScreen (Screen::Info); break;
case 7: enterScreen (Screen::Info); break;
default: break;
}
break;
case Screen::Patch:
proc.selectPatch (patchCursor);
enterScreen (Screen::Home);
enterScreen (Screen::Menu);
break;
case Screen::System:
switch (cursor)
@ -542,14 +803,17 @@ void LcdPanel::navEnter()
case 0: proc.getBank().writeTo (0, proc.apvts); proc.selectPatch (0); break;
case 1: proc.getEngine().allNotesOff (true); break;
case 2: break;
case 3: enterScreen (Screen::Home); break;
case 3: enterScreen (Screen::Menu); break;
default: break;
}
break;
case Screen::Info:
case Screen::Param:
break; // stay in the submenu; left/right edits the highlighted param
case Screen::Info:
enterScreen (Screen::Menu);
break;
default:
enterScreen (Screen::Home);
enterScreen (Screen::Menu);
break;
}
}

View file

@ -14,6 +14,7 @@ public:
void navUp(); void navDown();
void navLeft(); void navRight();
void navEnter(); void navExit();
void applyPatchOnBrowserMove();
void resetToHome();
@ -34,19 +35,28 @@ private:
int patchCursor = 0;
int paramScreenIdx = 0; // which param screen is open
float meterL = 0.0f, meterR = 0.0f;
float powerGlow = 0.0f;
int lastVoices = -1;
juce::uint32 lastMidi = 0;
juce::Image dotImage;
juce::Image dotImage; // 160x64 content scratch buffer
juce::Image displayImage; // 320x128 cached green-on-black result
juce::Image backdrop; // 320x128 static base (backlight + ghost dots)
bool contentDirty = true;
static const juce::uint8* glyphFor (int key);
static const juce::uint8* glyphM (int key);
void rebuild();
void buildBackdrop();
void drawContent (juce::Graphics&);
void px (juce::Graphics&, int x, int y, float intensity);
void pxText (juce::Graphics&, int x, int y, const juce::String&, float intensity);
void pxTextM (juce::Graphics&, int x, int y, const juce::String&, float intensity);
void drawFilled (juce::Graphics&, int x, int y, int w, int h, float intensity);
void drawHBar (juce::Graphics&, int x, int y, int w, double level, float intensity);
void drawSelBar (juce::Graphics&, int y);
void drawSplash (juce::Graphics&);
void drawHome (juce::Graphics&);
@ -57,9 +67,12 @@ private:
void drawInfo (juce::Graphics&);
void drawFooter (juce::Graphics&);
void stepParam (prm::Id id, int dir, bool coarse);
void selectPatchCyclic (int dir);
void setDirty() { dotImage.clear (dotImage.getBounds()); repaint(); }
void stepParam (prm::Id id, int dir, bool coarse);
void stepDelaySync (int dir);
void setDirty() { contentDirty = true; repaint(); }
void enterScreen (Screen s) { screen = s; cursor = 0; setDirty(); }
void timerCallback() override;

View file

@ -14,15 +14,15 @@ static const Desc table[num] = {
{ "fltres", "RESONANCE", "RES", 0, 0.98f, 0.18f, false, 0.01f, 0.08f },
{ "fltenv", "FILT ENV", "FEN", 0, 100, 52, false, 1, 8 },
{ "fltkey", "KEY TRACK", "KEY", 0, 100, 40, false, 1, 8 },
{ "ampA", "AMP ATTACK", "AAT", 0.001f, 6, 0.008f, false, 0.001f, 0.05f },
{ "ampD", "AMP DECAY", "ADE", 0.001f, 6, 0.26f, false, 0.001f, 0.05f },
{ "ampA", "AMP ATTACK", "AAT", 0.001f, 6, 0.008f, false, 0.01f, 0.1f },
{ "ampD", "AMP DECAY", "ADE", 0.001f, 6, 0.26f, false, 0.01f, 0.1f },
{ "ampS", "AMP SUSTAIN", "ASUS", 0, 1, 0.70f, false, 0.01f, 0.08f },
{ "ampR", "AMP RELEASE", "AREL", 0.001f, 8, 0.35f, false, 0.001f, 0.05f },
{ "fenvA", "FENV ATTACK", "FAT", 0.001f, 6, 0.02f, false, 0.001f, 0.05f },
{ "fenvD", "FENV DECAY", "FDE", 0.001f, 6, 0.16f, false, 0.001f, 0.05f },
{ "ampR", "AMP RELEASE", "AREL", 0.001f, 8, 0.35f, false, 0.01f, 0.1f },
{ "fenvA", "FENV ATTACK", "FAT", 0.001f, 6, 0.02f, false, 0.01f, 0.1f },
{ "fenvD", "FENV DECAY", "FDE", 0.001f, 6, 0.16f, false, 0.01f, 0.1f },
{ "fenvS", "FENV SUSTAIN", "FSUS", 0, 1, 0.26f, false, 0.01f, 0.08f },
{ "fenvR", "FENV RELEASE", "FRE", 0.001f, 8, 0.25f, false, 0.001f, 0.05f },
{ "lforate", "LFO RATE", "LFR", 0.02f, 22, 4.2f, false, 0.02f, 0.5f },
{ "fenvR", "FENV RELEASE", "FRE", 0.001f, 8, 0.25f, false, 0.01f, 0.1f },
{ "lforate", "LFO RATE", "LFR", 0.02f, 22, 4.2f, false, 0.1f, 0.5f },
{ "lfoshape", "LFO SHAPE", "LFS", 0, 3, 0, true, 1, 1 },
{ "lfodepth", "LFO DEPTH", "LFD", 0, 1, 0.22f, false, 0.01f, 0.08f },
{ "lfotarget", "LFO TARGET", "LFT", 0, 2, 0, true, 1, 1 },
@ -34,6 +34,7 @@ static const Desc table[num] = {
{ "poly", "POLYPHONY", "POLY", 1, 16, 10, true, 1, 1 },
{ "glide", "PORTAMENTO", "GLD", 0, 2, 0, false, 0.02f, 0.2f },
{ "tune", "MASTER TUNE", "TUN", -12, 12, 0, true, 1, 1 },
{ "hq", "HQ MODE", "HQ", 0, 1, 0, true, 1, 1 },
};
const Desc& desc (Id id) { return table[static_cast<int>(id)]; }
@ -59,7 +60,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout createLayout()
d.id, d.name, r, p0.v[i]));
}
layout.add (std::make_unique<juce::AudioParameterInt> ("patchsel", "Patch", 0, 15, 0));
layout.add (std::make_unique<juce::AudioParameterInt> ("patchsel", "Patch", 0, bank.size() - 1, 0));
layout.add (std::make_unique<juce::AudioParameterInt> ("delaysync", "Delay Sync", 0, 10, 0));
return layout;
}
@ -93,6 +95,49 @@ const char* fxTypeName (int t)
case 3: return "SPACE"; default: return "OFF"; }
}
const char* delaySyncName (int s)
{
switch (s)
{
case 0: return "OFF ";
case 1: return "1/16";
case 2: return "1/8 ";
case 3: return "1/8T";
case 4: return "1/8D";
case 5: return "1/4 ";
case 6: return "1/4T";
case 7: return "1/4D";
case 8: return "1/2 ";
case 9: return "3/4 ";
case 10: return "1/1 ";
default: return "--";
}
}
double delaySyncBeats (int s)
{
switch (s)
{
case 0: return 0.0; // disabled
case 1: return 0.25; // 1/16 note
case 2: return 0.5; // 1/8
case 3: return 1.0 / 3.0; // 1/8 triplet
case 4: return 0.75; // 1/8 dotted
case 5: return 1.0; // 1/4
case 6: return 2.0 / 3.0; // 1/4 triplet
case 7: return 1.5; // 1/4 dotted
case 8: return 2.0; // 1/2
case 9: return 3.0; // 3/4
case 10: return 4.0; // whole
default: return 0.0;
}
}
float delaySyncMaxTime()
{
return 5.0f; // allow a whole note at slow tempos (e.g. 48 BPM)
}
static juce::String fmtTime (float t)
{
if (t >= 1.0f) return juce::String (t, 2) + "s";
@ -138,6 +183,7 @@ juce::String formatParam (Id id, float v)
const int t = (int) v;
return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST";
}
case Id::hq: return v >= 0.5f ? "HQ" : "ECO";
default: return "-----";
}
}

View file

@ -10,10 +10,11 @@ enum class Id : int {
fenvA, fenvD, fenvS, fenvR,
lfoRate, lfoShape, lfoDepth, lfoTarget,
fxType, fxAmt, fxTime,
vol, pan, poly, glide, tune
vol, pan, poly, glide, tune,
hq
};
constexpr int num = 29;
constexpr int num = 30;
struct Desc {
const char* id;
@ -38,4 +39,9 @@ const char* lfoShapeName (int s);
const char* lfoTargetName (int t);
const char* fxTypeName (int t);
constexpr int delaySyncCount = 11;
const char* delaySyncName (int s); // "OFF", "1/16", ...
double delaySyncBeats (int s); // beat multiple for sync delay
float delaySyncMaxTime(); // max delay seconds needed for slow sync
} // namespace prm

View file

@ -9,6 +9,9 @@ RackEditor::RackEditor (RMX19AudioProcessor& p)
{
face = juce::Colour (0xff16130f);
backgroundImage = juce::ImageFileFormat::loadFrom (BinaryData::b2_png,
BinaryData::b2_pngSize);
up = new NavButton (-1, "^", "UP");
down = new NavButton (1, "v", "DOWN");
left = new NavButton (-1, "<", "LEFT");
@ -30,8 +33,43 @@ RackEditor::RackEditor (RMX19AudioProcessor& p)
addAndMakeVisible (display);
scaleBox.addItem ("50%", 1);
scaleBox.addItem ("75%", 2);
scaleBox.addItem ("100%", 3);
scaleBox.addItem ("125%", 4);
scaleBox.addItem ("150%", 5);
scaleBox.addItem ("200%", 6);
scaleBox.addItem ("250%", 7);
scaleBox.addItem ("300%", 8);
scaleBox.setSelectedId (5, juce::dontSendNotification);
scaleBox.setJustificationType (juce::Justification::centred);
scaleBox.setTextWhenNothingSelected ("UI scale");
scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colour (0xff221d18));
scaleBox.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xff6b5f4e));
scaleBox.setColour (juce::ComboBox::textColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::PopupMenu::backgroundColourId, juce::Colour (0xff1c1813));
scaleBox.setColour (juce::PopupMenu::textColourId, juce::Colour (0xffd8cdb9));
scaleBox.setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour (0xff3a332b));
scaleBox.onChange = [this]
{
switch (scaleBox.getSelectedId())
{
case 1: setScale (0.5f); break;
case 2: setScale (0.75f); break;
case 3: setScale (1.0f); break;
case 4: setScale (1.25f); break;
case 5: setScale (1.5f); break;
case 6: setScale (2.0f); break;
case 7: setScale (2.5f); break;
case 8: setScale (3.0f); break;
default: break;
}
};
addAndMakeVisible (scaleBox);
setWantsKeyboardFocus (true);
setSize (656, 248);
setScale (1.5f);
startTimerHz (18);
}
@ -42,11 +80,41 @@ RackEditor::~RackEditor()
void RackEditor::paint (juce::Graphics& g)
{
const auto b = getLocalBounds();
// full-area backdrop so scaled edges stay covered
g.fillAll (juce::Colour (0xff0d0b09));
// --- metal rack face ---
const auto b = juce::Rectangle<int> (0, 0, baseW, baseH);
// --- metal rack face (b2.png background, cover-scaled) ---
if (backgroundImage.isValid())
{
const float imageAspect = (float) backgroundImage.getWidth() / (float) backgroundImage.getHeight();
const float boxAspect = (float) b.getWidth() / (float) b.getHeight();
int sx = 0, sy = 0, sw = backgroundImage.getWidth(), sh = backgroundImage.getHeight();
if (imageAspect > boxAspect)
{
sw = (int) (backgroundImage.getHeight() * boxAspect);
sx = (backgroundImage.getWidth() - sw) / 2;
}
else
{
sh = (int) (backgroundImage.getWidth() / boxAspect);
sy = (backgroundImage.getHeight() - sh) / 2;
}
g.drawImage (backgroundImage, b.getX(), b.getY(),
juce::roundToInt (b.getWidth() * uiScale),
juce::roundToInt (b.getHeight() * uiScale),
sx, sy, sw, sh);
}
else
{
g.setColour (face);
g.fillRect (b);
}
g.setColour (juce::Colour (0xff08));
g.addTransform (juce::AffineTransform::scale (uiScale));
// brushed texture
g.setColour (juce::Colour::fromRGBA (255, 255, 255, 7));
@ -61,70 +129,62 @@ void RackEditor::paint (juce::Graphics& g)
g.fillRect (0, 4, b.getWidth(), 1);
g.fillRect (0, b.getHeight() - 5, b.getWidth(), 1);
// screw holes
const auto screw = [&] (int x, int y)
{
g.setColour (juce::Colour (0xff2a2620));
g.fillEllipse (x - 4, y - 4, 8, 8);
g.setColour (juce::Colour (0xff8a7f6e));
g.drawEllipse (x - 4.5f, y - 4.5f, 9, 9, 1.0f);
g.drawLine (x - 3, y, x + 3, y, 1.0f);
};
screw (8, 8); screw (b.getWidth() - 9, 8);
screw (8, b.getHeight() - 9); screw (b.getWidth() - 9, b.getHeight() - 9);
// brand plate
g.setColour (juce::Colour (0xff614f3a));
g.fillRect (12, 6, 150, 12);
g.setColour (juce::Colour (0xffc9a86f));
g.setFont (juce::FontOptions ("Arial", 10.0f, juce::Font::bold));
g.drawFittedText ("RMX-19 RACK SYNTH", 12, 6, 150, 12, juce::Justification::centred, 1);
g.drawFittedText ("RMX-19", 12, 6, 150, 12, juce::Justification::centred, 1);
// power LED (right of LCD status)
const int ledX = getWidth() - 20, ledY = 18;
g.setColour (juce::Colour (0xff3a0d0d));
g.fillEllipse (ledX - 3, ledY - 3, 8, 8);
g.setColour (juce::Colour::fromFloatRGBA (0.95f, 0.25f + 0.1f * powerPulse, 0.1f, 1.0f));
g.fillEllipse (ledX - 2, ledY - 2, 6, 6);
g.setColour (juce::Colour (0xff8a7f6e));
g.setFont (juce::FontOptions ("Arial", 8.0f, juce::Font::plain));
g.drawFittedText ("PWR", ledX - 26, ledY - 5, 20, 10, juce::Justification::centredRight, 1);
// rear engraving text
// rear engraving text (kept clear of the UI-scale dropdown at the bottom right)
g.setColour (juce::Colour (0xff6e6657));
g.setFont (juce::FontOptions ("Courier New", 8.0f, juce::Font::plain));
const juce::String ser = "S/N 0001-1943 MIDI OUT (R) MIDI IN (R) AUDIO 24BIT/96K";
g.drawFittedText (ser, 12, getHeight() - 16, getWidth() - 24, 11, juce::Justification::centred, 1);
g.drawFittedText (ser, 12, b.getHeight() - 16, b.getWidth() - 128, 11, juce::Justification::centred, 1);
}
void RackEditor::resized()
{
const auto b = getLocalBounds();
// UI-scale dropdown lives in real (unscaled) coordinates, bottom right.
scaleBox.setBounds (getWidth() - 112, getHeight() - 30, 104, 20);
// everything else renders in base 656x248 space, scaled by an affine transform
const juce::AffineTransform tf = juce::AffineTransform::scale (uiScale);
// LCD recessed module
const juce::Rectangle<int> lcdArea (14, 24, 356, 168);
display.setTransform (tf);
display.setBounds (lcdArea);
// nav diamond + enter/back to the right of the LCD
const juce::Rectangle<int> navArea (lcdArea.getRight() + 16, 26, b.getWidth() - lcdArea.getRight() - 30, 164);
const juce::Rectangle<int> navArea (lcdArea.getRight() + 16, 26, baseW - lcdArea.getRight() - 30, 164);
const int bw = 58, bh = 20;
const int cx = navArea.getX() + (navArea.getWidth() - bw) / 2;
const int rowY = navArea.getY();
up->setBounds (cx, rowY, bw, bh);
left->setBounds (navArea.getX() + 2, rowY + bh + 6, bw, bh);
down->setBounds (cx, rowY + bh + 6, bw, bh);
right->setBounds (navArea.getRight() - bw - 2, rowY + bh + 6, bw, bh);
up->setTransform (tf); up->setBounds (cx + 6, rowY + 1, bw + 3, bh);
left->setTransform (tf); left->setBounds (navArea.getX() + 1, rowY + bh + 6, bw + 2, bh);
down->setTransform (tf); down->setBounds (cx + 6, rowY + bh + 6 + 1, bw + 3, bh);
right->setTransform (tf); right->setBounds (navArea.getRight() - bw + 3, rowY + bh + 6, bw - 3, bh);
enterB->setBounds (navArea.getX() + 2, rowY + 2 * (bh + 6) + 4, bw, bh);
backB->setBounds (navArea.getRight() - bw - 2, rowY + 2 * (bh + 6) + 4, bw, bh);
enterB->setTransform (tf); enterB->setBounds (navArea.getX() + 2, rowY + 2 * (bh + 6) + 4, bw + 2, bh);
backB->setTransform (tf); backB->setBounds (navArea.getRight() - bw + 3, rowY + 2 * (bh + 6) + 4, bw - 3, bh);
}
void RackEditor::setScale (float s)
{
uiScale = s;
setSize (juce::roundToInt (baseW * s), juce::roundToInt (baseH * s));
resized(); // ensure child layouts refresh at the new size
}
void RackEditor::timerCallback()
{
powerPulse = 0.5f + 0.5f * std::sin (juce::Time::getMillisecondCounter() * 0.0022f);
repaint (getLocalBounds());
// only the pulsing power LED needs redraws; everything else is static
const int ledX = getWidth() - 20, ledY = 18;
repaint (ledX - 26, ledY - 7, 30, 14);
}
bool RackEditor::keyPressed (const juce::KeyPress& k)

View file

@ -2,6 +2,7 @@
#include <juce_gui_basics/juce_gui_basics.h>
#include "LcdPanel.h"
#include "PluginProcessor.h"
#include "BinaryData.h"
class RackEditor : public juce::AudioProcessorEditor, private juce::Timer
{
@ -42,11 +43,17 @@ private:
NavButton* left = nullptr; NavButton* right = nullptr;
OkButton* enterB = nullptr; OkButton* backB = nullptr;
juce::ComboBox scaleBox;
LcdPanel display;
juce::Colour face;
juce::Image backgroundImage;
float powerPulse = 0.0f;
float uiScale = 1.5f;
static constexpr int baseW = 656, baseH = 248;
void setScale (float s);
void timerCallback() override;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RackEditor)

View file

@ -3,6 +3,16 @@
// ---------------------------------------------------------------------------
static inline void dbgNote (const char* ev, int note, float vel)
{
juce::File (juce::File::getSpecialLocation (juce::File::userHomeDirectory)
.getChildFile ("rmx19_notes.log"))
.appendText (juce::String ("t=") + juce::String ((juce::int64) juce::Time::getMillisecondCounter())
+ " " + ev + " n=" + juce::String (note)
+ (vel >= 0.0f ? juce::String (" v=") + juce::String (vel, 2) : juce::String())
+ "\n");
}
class RMX19AudioProcessor::FxEngine
{
public:
@ -14,7 +24,7 @@ public:
delL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
delR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
const int maxDelay = (int) (sampleRate * 1.5);
const int maxDelay = (int) (sampleRate * prm::delaySyncMaxTime());
chorusL.setMaximumDelayInSamples (maxDelay);
chorusR.setMaximumDelayInSamples (maxDelay);
delL.setMaximumDelayInSamples (maxDelay);
@ -28,7 +38,7 @@ public:
chPh = 0.0;
}
void process (const VParams& p, float* L, float* R, int num)
void process (const VParams& p, float* L, float* R, int num, int syncIdx, double bpm)
{
const int type = (int) p.fxType;
if (type == 0) return;
@ -37,7 +47,9 @@ public:
const float dry = 1.0f - wet * 0.5f;
const float chBase = 0.008f * (float) sr; // 8 ms chorus base
const float chDepth = 0.0035f * (float) sr; // +-3.5 ms wobble
const float delSamples = (float) (p.fxTime * sr);
float delSamples = (float) (p.fxTime * sr);
if (syncIdx > 0 && bpm > 1.0)
delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr);
const float feedback = 0.40f;
for (int s = 0; s < num; ++s)
@ -104,6 +116,7 @@ void RMX19AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
engine.allNotesOff (true);
fx = std::make_unique<FxEngine>();
fx->prepare (sampleRate, samplesPerBlock);
osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true);
}
void RMX19AudioProcessor::releaseResources()
@ -125,10 +138,12 @@ void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
if (msg.isNoteOn())
{
engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
dbgNote ("ON", msg.getNoteNumber(), msg.getFloatVelocity());
}
else if (msg.isNoteOff())
{
engine.noteOff (msg.getNoteNumber());
dbgNote ("OFF", msg.getNoteNumber(), -1.0f);
}
else if (msg.isPitchWheel())
{
@ -141,7 +156,10 @@ void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
switch (cc)
{
case 64: engine.sustainPedal (msg.getControllerValue() >= 64); break;
case 1: engine.modWheel (v); break;
case 1: if (auto* par = apvts.getParameter ("fltcut")) par->setValueNotifyingHost (v); break;
case 2: if (auto* par = apvts.getParameter ("fltres")) par->setValueNotifyingHost (v); break;
case 3: if (auto* par = apvts.getParameter ("lforate")) par->setValueNotifyingHost (v); break;
case 4: if (auto* par = apvts.getParameter ("fxamt")) par->setValueNotifyingHost (v); break;
case 7: if (auto* par = apvts.getParameter ("vol")) par->setValueNotifyingHost (v); break;
case 10: if (auto* par = apvts.getParameter ("pan")) par->setValueNotifyingHost (v * 2.0f - 1.0f); break;
case 71: if (auto* par = apvts.getParameter ("fltres")) par->setValueNotifyingHost (v); break;
@ -169,6 +187,18 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
VParams p;
p.update (apvts);
// host tempo (fall back to 120 if not playing / unknown)
double bpm = 120.0;
if (auto* ph = getPlayHead())
if (auto pos = ph->getPosition())
if (pos->getIsPlaying() && pos->getBpm().hasValue())
bpm = juce::jlimit (20.0, 300.0, *pos->getBpm());
const int syncIdx = (int) apvts.getRawParameterValue ("delaysync")->load();
const bool hq = p.hq >= 0.5f;
const double engineSr = hq ? getSampleRate() * 2.0 : getSampleRate();
engine.setSampleRate (engineSr);
// patch follow
const int sel = (int) apvts.getRawParameterValue ("patchsel")->load();
if (sel != lastPatchParam && sel >= 0 && sel < bank.size())
@ -188,9 +218,37 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
buffer.clear (c, 0, numSamples);
if (L != nullptr && R != nullptr)
{
if (hq)
{
osBuf.clear();
engine.render (p, osBuf.getWritePointer (0), osBuf.getWritePointer (1), numSamples * 2);
for (int s = 0; s < numSamples; ++s)
{
const int o = s * 2;
L[s] = 0.5f * (osBuf.getSample (0, o) + osBuf.getSample (0, o + 1));
R[s] = 0.5f * (osBuf.getSample (1, o) + osBuf.getSample (1, o + 1));
}
}
else
{
engine.render (p, L, R, numSamples);
if (fx) fx->process (p, L, R, numSamples);
}
if (fx) fx->process (p, L, R, numSamples, syncIdx, bpm);
// loud-pop protection: remove DC steps and soft-clamp any blowout
const float dcCoef = 0.9995f;
for (int s = 0; s < numSamples; ++s)
{
const float tL = std::tanh (L[s]);
const float tR = std::tanh (R[s]);
const float oL = tL - dcPrevL + dcCoef * dcL;
const float oR = tR - dcPrevR + dcCoef * dcR;
dcPrevL = tL; dcPrevR = tR;
dcL = oL; dcR = oR;
L[s] = oL;
R[s] = oR;
}
if (p.vol != 1.0f) buffer.applyGain (p.vol);
}
else if (L != nullptr)
@ -198,14 +256,44 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
juce::AudioBuffer<float> dummy (1, numSamples);
dummy.clear();
float* dL = dummy.getWritePointer (0);
if (hq)
{
osBuf.clear();
engine.render (p, osBuf.getWritePointer (0), osBuf.getWritePointer (1), numSamples * 2);
for (int s = 0; s < numSamples; ++s)
{
const int o = s * 2;
dL[s] = 0.5f * (osBuf.getSample (0, o) + osBuf.getSample (1, o)
+ osBuf.getSample (0, o + 1) + osBuf.getSample (1, o + 1)) * 0.5f;
}
}
else
{
float* dR = dummy.getWritePointer (0);
engine.render (p, dL, dR, numSamples);
}
if (fx) fx->process (p, dL, dL, numSamples, syncIdx, bpm);
for (int s = 0; s < numSamples; ++s)
{
dL[s] = std::tanh (dL[s] * 0.5f);
}
for (int c = 0; c < numChannels; ++c)
buffer.copyFrom (c, 0, dummy, 0, 0, numSamples);
buffer.applyGain (p.vol);
}
cpu = 0.7f * cpu + 0.3f * (float) (juce::Time::getMillisecondCounter() - start);
// real-time health check (offline probe + debug): flag blocks near the limit
const float blockMs = (numSamples == 0) ? 0.0f
: (float) numSamples / (float) std::max (1.0, getSampleRate()) * 1000.0f;
const float usedMs = (float) (juce::Time::getMillisecondCounter() - start);
if (usedMs > 0.5f * blockMs)
juce::File (juce::File::getSpecialLocation (juce::File::userHomeDirectory)
.getChildFile ("rmx19_notes.log"))
.appendText (juce::String ("OVER t=") + juce::String ((juce::int64) juce::Time::getMillisecondCounter())
+ " used=" + juce::String (usedMs, 2)
+ "ms budget=" + juce::String (blockMs, 2) + "ms\n");
}
void RMX19AudioProcessor::selectPatch (int i)

View file

@ -22,7 +22,7 @@ public:
const juce::String getName() const override;
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
double getTailLengthSeconds() const override { return 3.0; }
double getTailLengthSeconds() const override { return prm::delaySyncMaxTime() + 1.0; }
int getNumPrograms() override;
int getCurrentProgram() override;
@ -37,10 +37,8 @@ public:
PresetBank& getBank() { return bank; }
SynthEngine& getEngine() { return engine; }
int getPatchIndex() const noexcept { return patchIndex; }
int getPatchIndex() const noexcept { return patchIndex; }
void selectPatch (int i);
void getMeters (float& l, float& r) const noexcept { engine.getMeters (l, r); }
int getActiveVoices() const noexcept { return engine.activeVoices(); }
juce::uint32 getLastMidiTime() const noexcept { return lastMidi; }
float getCpu() const noexcept { return cpu; }
@ -59,6 +57,8 @@ private:
int lastPatchParam = 0;
juce::uint32 lastMidi = 0;
float cpu = 0.0f;
float dcL = 0.0f, dcR = 0.0f, dcPrevL = 0.0f, dcPrevR = 0.0f;
juce::AudioBuffer<float> osBuf; // internal 2x oversample target (HQ mode)
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RMX19AudioProcessor)
};

View file

@ -19,112 +19,280 @@ static const Patch defs[] = {
0.40f, 0.9f, 0.35f, 0.5f,
4.0f, 0, 0.18f, 0,
1, 0.32f, 0.30f,
0.82f, 0.0f, 12, 0.10f, 0 } },
0.82f, 0.0f, 12, 0.0f, 0, 0 } },
// 2 Glass Pad --------------------------------------------------------
{ "Glass Pad", "POLY PAD", { O_TRI, O_TRI, 12, 60, 2, 1400, 0.55f, 75, 30,
0.80f, 1.8f, 0.90f, 1.60f,
0.50f, 1.2f, 0.40f, 0.8f,
5.2f, 0, 0.16f, 2,
3, 0.38f, 0.40f,
0.80f, 0.0f, 12, 0.10f, 0 } },
0.92f, 0.0f, 12, 0.0f, 0, 0 } },
// 3 Dream Pad --------------------------------------------------------
{ "Dream Pad", "POLY PAD", { O_SAW, O_PUL, -7, 50, 1, 3000, 0.25f, 70, 40,
1.10f, 2.4f, 0.85f, 1.80f,
0.60f, 1.4f, 0.30f, 0.9f,
0.35f, 1, 0.20f, 0,
1, 0.40f, 0.50f,
0.80f, 0.0f, 12, 0.15f, 0 } },
0.80f, 0.0f, 12, 0.0f, 0, 0 } },
// 4 Movie Strings ----------------------------------------------------
{ "Movie Strings", "POLY PAD", { O_SAW, O_SAW, 6, 45, 1, 2200, 0.15f, 65, 55,
0.50f, 1.2f, 0.80f, 0.70f,
0.40f, 0.8f, 0.35f, 0.5f,
3.1f, 0, 0.10f, 0,
3, 0.28f, 0.45f,
0.82f, 0.0f, 12, 0.08f, 0 } },
0.82f, 0.0f, 12, 0.0f, 0, 0 } },
// 5 Electric Piano ---------------------------------------------------
{ "Electric Piano", "KEY", { O_PUL, O_TRI, 5, 62, 1, 2800, 0.25f, 45, 40,
0.004f, 1.4f, 0.30f, 0.40f,
0.004f, 0.2f, 0.10f, 0.3f,
6.0f, 0, 0.05f, 2,
1, 0.35f, 0.30f,
0.80f, 0.0f, 12, 0.0f, 0 } },
0.80f, 0.0f, 12, 0.0f, 0, 0 } },
// 6 Acoustic Piano ---------------------------------------------------
{ "Acoustic Piano", "KEY", { O_SAW, O_TRI, 3, 55, 1, 5200, 0.18f, 30, 60,
0.003f, 2.2f, 0.25f, 0.50f,
0.002f, 0.25f, 0.05f, 0.35f,
4.0f, 0, 0.03f, 2,
1, 0.22f, 0.30f,
0.80f, 0.0f, 12, 0.0f, 0 } },
0.80f, 0.0f, 12, 0.0f, 0, 0 } },
// 7 Toy Piano --------------------------------------------------------
{ "Toy Piano", "KEY", { O_PUL, O_PUL, 0, 70, 2, 2100, 0.50f, 30, 45,
0.002f, 0.35f, 0.0f, 0.18f,
0.002f, 0.05f, 0.02f, 0.08f,
5.0f, 0, 0.03f, 2,
2, 0.18f, 0.17f,
0.78f, 0.0f, 12, 0.0f, 0 } },
0.90f, 0.0f, 12, 0.0f, 0, 0 } },
// 8 Clavinet 70 ------------------------------------------------------
{ "Clavinet 70", "KEY", { O_PUL, O_PUL, 0, 40, 2, 1500, 0.40f, 60, 30,
0.002f, 0.50f, 0.45f, 0.12f,
0.001f, 0.05f, 0.05f, 0.10f,
4.0f, 0, 0.05f, 0,
1, 0.30f, 0.28f,
0.84f, 0.0f, 12, 0.0f, 0 } },
0.92f, 0.0f, 12, 0.0f, 0, 0 } },
// 9 Organ Drawbar ----------------------------------------------------
{ "Organ Drawbar", "KEY", { O_SAW, O_SAW, 0, 50, 1, 10000, 0.10f, 20, 80,
0.010f, 0.10f, 0.98f, 0.10f,
0.010f, 0.10f, 0.50f, 0.10f,
8.0f, 3, 0.03f, 1,
1, 0.20f, 0.28f,
0.82f, 0.0f, 8, 0.0f, 0 } },
0.82f, 0.0f, 8, 0.0f, 0, 0 } },
// 10 FM E.Piano ------------------------------------------------------
{ "FM E.Piano", "KEY", { O_SIN, O_TRI, 0, 30, 1, 4000, 0.12f, 50, 50,
0.002f, 1.0f, 0.50f, 0.30f,
0.002f, 0.20f, 0.10f, 0.25f,
4.5f, 0, 0.04f, 2,
1, 0.40f, 0.30f,
0.78f, 0.0f, 12, 0.0f, 0 } },
0.86f, 0.0f, 12, 0.0f, 0, 0 } },
// 11 Analog Bass -----------------------------------------------------
{ "Analog Bass", "BASS", { O_PUL, O_SIN, 0, 80, 1, 700, 0.35f, 35, 10,
0.002f, 0.30f, 0.82f, 0.12f,
0.002f, 0.10f, 0.15f, 0.10f,
3.0f, 0, 0.05f, 0,
0, 0.0f, 0.30f,
0.90f, 0.0f, 1, 0.04f, 0 } },
0.94f, 0.0f, 1, 0.04f, 0, 0 } },
// 12 Sub Rez Bass ----------------------------------------------------
{ "Sub Rez Bass", "BASS", { O_SAW, O_PUL, -4, 45, 1, 520, 0.70f, 55, 15,
0.004f, 0.40f, 0.70f, 0.10f,
0.004f, 0.15f, 0.25f, 0.10f,
2.4f, 3, 0.30f, 0,
0, 0.0f, 0.30f,
0.90f, 0.0f, 2, 0.03f, 0 } },
0.94f, 0.0f, 1, 0.03f, 0, 0 } },
// 13 Saw Lead --------------------------------------------------------
{ "Saw Lead", "LEAD", { O_SAW, O_SAW, 14, 50, 1, 3800, 0.25f, 30, 60,
0.005f, 0.30f, 0.80f, 0.20f,
0.005f, 0.15f, 0.30f, 0.15f,
5.4f, 0, 0.14f, 1,
0, 0.0f, 0.28f,
0.86f, 0.0f, 4, 0.06f, 0 } },
0.86f, 0.0f, 1, 0.06f, 0, 0 } },
// 14 Mr. Square ------------------------------------------------------
{ "Mr. Square", "LEAD", { O_PUL, O_PUL, 7, 60, 1, 2400, 0.22f, 20, 70,
0.003f, 0.25f, 0.72f, 0.18f,
0.003f, 0.12f, 0.25f, 0.12f,
6.6f, 0, 0.10f, 1,
0, 0.0f, 0.25f,
0.84f, 0.0f, 4, 0.05f, 0 } },
0.84f, 0.0f, 1, 0.05f, 0, 0 } },
// 15 Choir Layered ---------------------------------------------------
{ "Choir Layered", "POLY PAD",{ O_SAW, O_PUL, 9, 48, 1, 2600, 0.14f, 62, 45,
0.35f, 0.80f, 0.85f, 1.10f,
0.30f, 0.60f, 0.35f, 0.60f,
3.4f, 1, 0.12f, 0,
1, 0.38f, 0.42f,
0.82f, 0.0f, 12, 0.08f, 0 } },
0.82f, 0.0f, 12, 0.0f, 0, 0 } },
// 16 8-bit Chiptune --------------------------------------------------
{ "8bit Chiptune", "LEAD", { O_PUL, O_PUL, 0, 30, 1, 9000, 0.10f, 10, 70,
0.003f, 0.12f, 0.90f, 0.12f,
0.003f, 0.06f, 0.10f, 0.08f,
9.0f, 0, 0.05f, 0,
0, 0.0f, 0.20f,
0.80f, 0.0f, 6, 0.0f, 0 } },
0.80f, 0.0f, 6, 0.0f, 0, 0 } },
// 17 Shimmer Pad -----------------------------------------------------
{ "Shimmer Pad", "POLY PAD", { O_TRI, O_TRI, 14, 55, 2, 1800, 0.50f, 60, 20,
0.50f, 2.0f, 0.85f, 2.0f,
0.30f, 1.5f, 0.40f, 1.0f,
5.2f, 0, 0.20f, 2,
3, 0.38f, 0.55f,
0.90f, 0.0f, 16, 0.0f, 0, 0 } },
// 18 Strings Ensemble -------------------------------------------------
{ "Strings Ens.", "POLY PAD", { O_SAW, O_PUL, 8, 58, 1, 3000, 0.10f, 65, 55,
0.25f, 1.2f, 0.82f, 1.8f,
0.20f, 0.7f, 0.40f, 0.6f,
3.8f, 0, 0.10f, 0,
1, 0.30f, 0.30f,
0.80f, 0.0f, 16, 0.0f, 0, 0 } },
// 19 Church Bell -----------------------------------------------------
{ "Church Bell", "KEY", { O_SIN, O_SIN, 0, 40, 2, 3200, 0.40f, 40, 30,
0.002f, 2.5f, 0.10f, 1.8f,
0.002f, 0.3f, 0.10f, 0.6f,
4.4f, 0, 0.05f, 2,
3, 0.30f, 0.60f,
0.92f, 0.0f, 10, 0.0f, 0, 0 } },
// 20 E.Piano 2 -------------------------------------------------------
{ "E.Piano 2", "KEY", { O_PUL, O_TRI, 4, 58, 1, 2500, 0.30f, 40, 35,
0.003f, 1.2f, 0.40f, 0.5f,
0.003f, 0.25f, 0.20f, 0.3f,
6.0f, 0, 0.04f, 2,
1, 0.35f, 0.30f,
0.80f, 0.0f, 12, 0.0f, 0, 0 } },
// 21 Acid Bass -------------------------------------------------------
{ "Acid Bass", "BASS", { O_SAW, O_PUL, 0, 72, 1, 650, 0.60f, 70, 0,
0.002f, 0.30f, 0.75f, 0.15f,
0.002f, 0.10f, 0.12f, 0.05f,
0.6f, 1, 0.30f, 0,
0, 0.0f, 0.30f,
0.90f, 0.0f, 1, 0.04f, 0, 0 } },
// 22 Analog Bass II --------------------------------------------------
{ "Analog Bass II", "BASS", { O_PUL, O_SIN, 0, 82, 1, 420, 0.40f, 45, 20,
0.002f, 0.25f, 0.80f, 0.12f,
0.002f, 0.12f, 0.20f, 0.08f,
4.0f, 0, 0.08f, 1,
1, 0.25f, 0.20f,
0.88f, 0.0f, 1, 0.08f, 0, 0 } },
// 23 Detuned Lead ----------------------------------------------------
{ "Detuned Lead", "LEAD", { O_SAW, O_SAW, 18, 45, 1, 4100, 0.20f, 35, 60,
0.004f, 0.22f, 0.85f, 0.16f,
0.004f, 0.12f, 0.30f, 0.10f,
6.8f, 0, 0.09f, 1,
2, 0.25f, 0.45f,
0.85f, 0.0f, 1, 0.05f, 0, 0 } },
// 24 Square Lead -----------------------------------------------------
{ "Square Lead", "LEAD", { O_PUL, O_PUL, 10, 55, 1, 5200, 0.25f, 30, 65,
0.004f, 0.35f, 0.72f, 0.20f,
0.004f, 0.15f, 0.25f, 0.12f,
7.2f, 0, 0.08f, 1,
1, 0.30f, 0.25f,
0.85f, 0.0f, 1, 0.06f, 0, 0 } },
// 25 Deep Space Pad --------------------------------------------------
{ "Deep Space Pad","POLY PAD", { O_SAW, O_SAW, 16, 60, 1, 1500, 0.45f, 70, 35,
0.90f, 2.4f, 0.85f, 2.2f,
0.50f, 1.6f, 0.45f, 1.2f,
5.0f, 0, 0.22f, 2,
3, 0.42f, 0.70f,
0.80f, 0.0f, 16, 0.0f, 0, 0 } },
// 26 Liquid Keys -----------------------------------------------------
{ "Liquid Keys", "KEY", { O_TRI, O_TRI, 8, 55, 1, 2800, 0.30f, 55, 30,
0.30f, 1.6f, 0.60f, 0.90f,
0.20f, 1.0f, 0.30f, 0.60f,
3.6f, 0, 0.16f, 2,
3, 0.36f, 0.50f,
0.84f, 0.0f, 12, 0.0f, 0, 0 } },
// 27 Techno Bass -----------------------------------------------------
{ "Techno Bass", "BASS", { O_SAW, O_PUL, 0, 88, 1, 480, 0.45f, 60, 10,
0.003f, 0.40f, 0.75f, 0.10f,
0.003f, 0.12f, 0.15f, 0.08f,
2.0f, 2, 0.35f, 0,
2, 0.32f, 0.28f,
0.92f, 0.0f, 1, 0.03f, 0, 0 } },
// 28 Octave Lead -----------------------------------------------------
{ "Octave Lead", "LEAD", { O_PUL, O_SAW, 0, 45, 1, 4200, 0.20f, 25, 65,
0.004f, 0.28f, 0.78f, 0.18f,
0.004f, 0.14f, 0.28f, 0.12f,
6.0f, 0, 0.12f, 1,
2, 0.28f, 0.35f,
0.86f, 0.0f, 1, 0.05f, 0, 0 } },
// 29 Glassy Chime ----------------------------------------------------
{ "Glassy Chime", "KEY", { O_SIN, O_TRI, 20, 65, 2, 4200, 0.35f, 35, 40,
0.005f, 2.0f, 0.30f, 1.6f,
0.004f, 0.35f, 0.15f, 0.6f,
5.6f, 0, 0.08f, 2,
3, 0.34f, 0.60f,
0.82f, 0.0f, 10, 0.0f, 0, 0 } },
// 30 Chorus Organ ----------------------------------------------------
{ "Chorus Organ", "KEY", { O_SAW, O_SAW, 0, 42, 1, 8000, 0.14f, 30, 75,
0.010f, 0.10f, 0.95f, 0.12f,
0.010f, 0.12f, 0.60f, 0.15f,
7.5f, 3, 0.04f, 1,
1, 0.40f, 0.34f,
0.86f, 0.0f, 8, 0.0f, 0, 0 } },
// 31 Night Strings ---------------------------------------------------
{ "Night Strings","POLY PAD", { O_SAW, O_TRI, 10, 52, 0, 2600, 0.10f, 60, 50,
1.10f, 2.6f, 0.85f, 2.0f,
0.55f, 1.6f, 0.40f, 1.1f,
3.2f, 0, 0.14f, 0,
3, 0.40f, 0.55f,
0.84f, 0.0f, 16, 0.0f, 0, 0 } },
// 32 Pulse Echo ------------------------------------------------------
{ "Pulse Echo", "LEAD", { O_PUL, O_PUL, 0, 35, 2, 6000, 0.18f, 15, 60,
0.005f, 0.32f, 0.70f, 0.20f,
0.005f, 0.16f, 0.30f, 0.15f,
8.0f, 0, 0.10f, 0,
2, 0.45f, 0.38f,
0.84f, 0.0f, 1, 0.04f, 0, 0 } },
// 33 Sine Sub Bass ---------------------------------------------------
{ "Sine Sub Bass","BASS", { O_SIN, O_SIN, 0, 100, 1, 380, 0.20f, 55, 10,
0.003f, 0.35f, 0.85f, 0.10f,
0.003f, 0.10f, 0.30f, 0.10f,
3.5f, 0, 0.06f, 0,
2, 0.25f, 0.20f,
0.90f, 0.0f, 1, 0.05f, 0, 0 } },
// 34 Space Choir -----------------------------------------------------
{ "Space Choir", "POLY PAD", { O_TRI, O_SAW, 12, 50, 1, 2400, 0.35f, 65, 45,
0.45f, 1.4f, 0.82f, 1.7f,
0.30f, 0.90f, 0.40f, 0.9f,
3.8f, 1, 0.15f, 0,
3, 0.46f, 0.65f,
0.82f, 0.0f, 12, 0.0f, 0, 0 } },
// 35 Retro Clav ------------------------------------------------------
{ "Retro Clav", "KEY", { O_PUL, O_TRI, 2, 55, 1, 2600, 0.35f, 40, 35,
0.003f, 0.60f, 0.40f, 0.15f,
0.002f, 0.10f, 0.08f, 0.12f,
5.0f, 0, 0.05f, 2,
3, 0.25f, 0.30f,
0.86f, 0.0f, 10, 0.0f, 0, 0 } },
// 36 High Strings ----------------------------------------------------
{ "High Strings", "POLY PAD", { O_SAW, O_PUL, 6, 60, 1, 3400, 0.10f, 55, 60,
0.20f, 1.0f, 0.80f, 1.6f,
0.15f, 0.60f, 0.40f, 0.7f,
4.2f, 0, 0.10f, 0,
1, 0.35f, 0.40f,
0.82f, 0.0f, 16, 0.0f, 0, 0 } },
// 37 Echo Flute ------------------------------------------------------
{ "Echo Flute", "LEAD", { O_TRI, O_SIN, 0, 85, 1, 3000, 0.15f, 20, 55,
0.05f, 0.50f, 0.75f, 0.25f,
0.04f, 0.30f, 0.40f, 0.20f,
4.8f, 0, 0.10f, 1,
2, 0.40f, 0.45f,
0.86f, 0.0f, 1, 0.06f, 0, 0 } },
// 38 Dub Rez Bass ----------------------------------------------------
{ "Dub Rez Bass", "BASS", { O_SAW, O_PUL, -5, 60, 1, 500, 0.65f, 45, 5,
0.004f, 0.35f, 0.80f, 0.12f,
0.004f, 0.15f, 0.25f, 0.12f,
0.8f, 3, 0.30f, 0,
2, 0.40f, 0.36f,
0.90f, 0.0f, 1, 0.05f, 0, 0 } },
// 39 Crystal Pad -----------------------------------------------------
{ "Crystal Pad", "POLY PAD", { O_SIN, O_TRI, 18, 55, 2, 3600, 0.40f, 50, 25,
0.60f, 2.2f, 0.85f, 2.0f,
0.40f, 1.4f, 0.40f, 1.2f,
5.5f, 0, 0.20f, 2,
3, 0.40f, 0.60f,
0.80f, 0.0f, 12, 0.0f, 0, 0 } },
// 40 Funk Wah --------------------------------------------------------
{ "Funk Wah", "KEY", { O_PUL, O_SAW, 0, 40, 1, 1800, 0.55f, 75, 20,
0.004f, 0.70f, 0.50f, 0.20f,
0.004f, 0.20f, 0.20f, 0.15f,
7.0f, 2, 0.40f, 0,
1, 0.32f, 0.30f,
0.88f, 0.0f, 1, 0.03f, 0, 0 } },
};
PresetBank::PresetBank()
@ -146,6 +314,9 @@ void PresetBank::writeTo (int i, juce::AudioProcessorValueTreeState& apvts) cons
i = juce::jlimit (0, n - 1, i);
const Patch& p = patches[i];
for (int k = 0; k < prm::num; ++k)
{
if (k == static_cast<int> (prm::Id::hq)) continue; // global, not per-patch
if (auto* par = apvts.getParameter (prm::desc (static_cast<prm::Id> (k)).id))
par->setValueNotifyingHost (par->convertTo0to1 (p.v[k]));
}
}

121
Source/Probe.cpp Normal file
View file

@ -0,0 +1,121 @@
// Offline probe: feed the real RMX19AudioProcessor MIDI and analyze output.
#include "PluginProcessor.h"
#include <cmath>
#include <cstdio>
#include <vector>
static int runScenario (int patch,
bool holdG,
int noteOffCAtBlock,
long* cutAt,
bool hqOn = false)
{
RMX19AudioProcessor proc;
proc.setRateAndBufferSizeDetails (48000.0, 512);
proc.prepareToPlay (48000.0, 512);
proc.selectPatch (patch);
if (hqOn)
if (auto* h = proc.apvts.getParameter ("hq"))
h->setValue (1.0f);
const int C = 60, G = 67;
const int totalBlocks = 240;
juce::AudioBuffer<float> buf (2, 512);
std::vector<float> peak (totalBlocks, 0.0f);
auto noteOn = [](int n, float v){ return juce::MidiMessage::noteOn (1, (juce::uint8) n, v); };
auto noteOff = [](int n){ return juce::MidiMessage::noteOff (1, (juce::uint8) n); };
for (int b = 0; b < totalBlocks; ++b)
{
juce::MidiBuffer midi;
const int cOn = 0, gOn = 40; // C held from block 0, G pressed at block 40
if (b == cOn) midi.addEvent (noteOn (C, 0.8f), 0);
if (b == gOn) midi.addEvent (noteOn (G, 0.8f), 0);
if (noteOffCAtBlock >= 0 && b == noteOffCAtBlock) midi.addEvent (noteOff (C), 0);
buf.clear();
proc.processBlock (buf, midi);
float p = 0.0f;
for (int s = 0; s < 512; ++s)
p = std::max (p, std::abs (buf.getSample (0, s)));
peak[b] = p;
}
proc.releaseResources();
// find the first block after G onset where amplitude collapses for a run
long cmax = 250;
float gPeak = 0.0f;
for (int b = 41; b < totalBlocks; ++b) gPeak = std::max (gPeak, peak[b]);
// detect a "cut": after G onset, a gap where peak < 2% of max for >= 8 blocks
int run = 0;
for (int b = 45; b < totalBlocks; ++b)
{
if (peak[b] < 0.02f * gPeak + 1e-5f)
{
if (++run >= 8)
{
cmax = b - run;
break;
}
}
else run = 0;
}
*cutAt = cmax;
printf ("patch %2d | G peak=%.3f cut-at-block=%s\n", patch, gPeak,
cmax >= 250 ? "NONE" : std::to_string (cmax).c_str());
return (int) gPeak;
}
int main()
{
long cut = -1;
// patch 18 Strings Ens.: C held, G pressed -> does G survive?
runScenario (18, true, -1, &cut);
// same but release C while G is held
runScenario (18, true, 80, &cut);
// sanity: a working patch
runScenario (1, true, -1, &cut);
runScenario (4, true, -1, &cut);
runScenario (18, true, -1, &cut, /*hqOn*/ true);
// cover more held keys after C+G while they are held (fills all 16 voices)
{
RMX19AudioProcessor proc;
proc.setRateAndBufferSizeDetails (48000.0, 512);
proc.prepareToPlay (48000.0, 512);
proc.selectPatch (18);
const int totalBlocks = 240;
juce::AudioBuffer<float> buf (2, 512);
std::vector<float> peak (totalBlocks, 0.0f);
std::vector<float> perBlock (totalBlocks, 1.0f);
for (int b = 0; b < totalBlocks; ++b)
{
juce::MidiBuffer midi;
if (b == 0) midi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.8f), 0);
if (b == 40) midi.addEvent (juce::MidiMessage::noteOn (1, 67, 0.8f), 0);
// after G, spread another 14 held notes so all 16 voices are busy
if (b >= 45 && b < 59) midi.addEvent (juce::MidiMessage::noteOn (1, (juce::uint8)(36 + (b - 45)), 0.8f), 0);
buf.clear();
proc.processBlock (buf, midi);
float p = 0.0f;
for (int s = 0; s < 512; ++s) p = std::max (p, std::abs (buf.getSample (0, s)));
peak[b] = p;
}
// sustain = peak never collapses below 5% of the pre-collapse max for a long stretch
float m50 = 0.0f;
for (int b = 41; b < 120; ++b) m50 = std::max (m50, peak[b]);
int lowRun = 0, maxLowRun = 0;
for (int b = 60; b < totalBlocks; ++b)
{
if (peak[b] < 0.05f * m50) { ++lowRun; maxLowRun = std::max (maxLowRun, lowRun); }
else lowRun = 0;
}
printf ("16-voice fill: block20max=%.3f maxSilentRun=%d blocks %s\n",
m50, maxLowRun, maxLowRun >= 12 ? "<- CUT detected" : "sustained");
proc.releaseResources();
}
return 0;
}

View file

@ -64,8 +64,9 @@ SynthEngine::Voice* SynthEngine::acquireVoice()
void SynthEngine::noteOn (int midiNote, float velocity)
{
const float noteFreq = (float) (440.0 * std::pow (2.0, (midiNote - 69.0) / 12.0));
const float noteFreq = (float) (440.0 * std::exp2 ((midiNote - 69.0) / 12.0));
Voice* v = acquireVoice();
const bool fresh = ! v->active;
v->active = true;
v->note = midiNote;
v->vel = 1.0f - std::pow (1.0f - velocity, 1.35f);
@ -77,6 +78,7 @@ void SynthEngine::noteOn (int midiNote, float velocity)
v->lfoIdx = 0;
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
v->lp = v->bp = 0.0f;
v->fcSm = 0.0f;
v->outSmooth = 0.0f;
const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f);
@ -88,7 +90,7 @@ void SynthEngine::noteOn (int midiNote, float velocity)
v->amp.reset(); v->flt.reset();
v->amp.noteOn(); v->flt.noteOn();
lastNoteFreq = noteFreq;
++active;
if (fresh) ++active;
}
void SynthEngine::noteOff (int midiNote)
@ -106,9 +108,6 @@ void SynthEngine::noteOff (int midiNote)
void SynthEngine::render (const VParams& p, float* left, float* right, int num)
{
float peakL = metL, peakR = metR;
metL = metR = 0.0f;
for (auto& v : voices)
{
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
@ -120,8 +119,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
const float glideRate = p.glide <= 0.001f ? 1.0f
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
const float detuneRatio = std::pow (2.0f, p.detune / 1200.0f);
const float tuneRatio = std::pow (2.0f, p.tune / 12.0f);
const float detuneRatio = std::exp2f (p.detune / 1200.0f);
const float tuneRatio = std::exp2f (p.tune / 12.0f);
const float maxFc = 18000.0f, minFc = 30.0f;
for (int s = 0; s < num; ++s)
@ -160,10 +159,10 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// frequency, vibrato, bend, tune
float freq = v.curFreq * tuneRatio;
if ((int) p.lfoTarget == 1)
freq *= std::pow (2.0f, lfo * p.lfoDepth * 1.6f / 12.0f);
freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f);
if (bendSmooth != bendCents)
bendSmooth += (bendCents - bendSmooth) * 0.08f;
freq *= std::pow (2.0f, bendSmooth / 1200.0f);
freq *= std::exp2f (bendSmooth / 1200.0f);
const float d1 = (float) (freq / sr);
const float d2 = d1 * detuneRatio;
@ -180,16 +179,19 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// cutoff with keytrack + filter envelope + LFO + mod wheel
float fc = p.fltCut;
fc *= std::pow (freq / 261.6256f, p.fltKey * 0.01f);
fc *= std::pow (maxFc / fc, p.fltEnv * 0.01f * fe);
fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f));
fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
if ((int) p.lfoTarget == 0)
fc *= std::pow (2.0f, lfo * p.lfoDepth * 10.0f / 12.0f);
fc *= std::pow (2.0f, mod);
fc *= std::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f);
fc *= std::exp2f (mod);
fc = std::min (maxFc, std::max (minFc, fc));
v.fcSm += (fc - v.fcSm) * 0.25f;
fc = v.fcSm;
// SVF
const float f1 = 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr);
const float q1 = 1.0f / (0.5f + p.fltRes * 3.5f);
// SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the
// resonance feedback can self-oscillate and sputter at maxed cutoff+res)
const float f1 = std::min (1.50f, 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr));
const float q1 = 1.0f / (1.0f + p.fltRes * 3.0f);
v.f1c = f1;
const float hp = sig - v.lp - q1 * v.bp;
v.bp += f1 * hp;
@ -211,7 +213,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
v.outSmooth += (filtered - v.outSmooth) * 0.12f;
const float o = v.outSmooth * (ae * ampMod) * v.vel;
float o = v.outSmooth * (ae * ampMod) * v.vel;
if (! std::isfinite (o)) o = 0.0f;
mixL += o * v.panL;
mixR += o * v.panR;
@ -223,13 +226,12 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
}
}
peakL = std::max (peakL, std::abs (mixL));
peakR = std::max (peakR, std::abs (mixR));
if (! std::isfinite (mixL)) mixL = 0.0f;
if (! std::isfinite (mixR)) mixR = 0.0f;
mixL = std::tanh (mixL);
mixR = std::tanh (mixR);
left[s] += mixL;
right[s] += mixR;
}
metL = peakL;
metR = peakR;
}

View file

@ -11,6 +11,7 @@ struct VParams
float lfoRate = 4.2f, lfoShape = 0, lfoDepth = 0.22f, lfoTarget = 0;
float fxType = 1, fxAmt = 0.30f, fxTime = 0.28f;
float vol = 0.85f, pan = 0.0f, poly = 10.f, glide = 0.f, tune = 0.f;
float hq = 0.0f;
void update (const juce::AudioProcessorValueTreeState& apvts)
{
@ -28,6 +29,7 @@ struct VParams
fxType = get ("fxtype"); fxAmt = get ("fxamt"); fxTime = get ("fxtime");
vol = get ("vol"); pan = get ("pan"); poly = get ("poly");
glide = get ("glide"); tune = get ("tune");
hq = get ("hq");
}
};
@ -108,7 +110,6 @@ public:
void render (const VParams& p, float* left, float* right, int numSamples);
int activeVoices() const noexcept { return active; }
void getMeters (float& l, float& r) const noexcept { l = metL; r = metR; }
private:
struct Voice
@ -123,6 +124,7 @@ private:
int lfoIdx = 0;
Env amp, flt;
float lp = 0.0f, bp = 0.0f, f1c = 0.0f;
float fcSm = 0.0f;
float outSmooth = 0.0f;
};
@ -135,7 +137,6 @@ private:
bool pedal = false;
float lastNoteFreq = 440.0f, lastGlide = 0.0f, lastPan = 0.0f;
int stealIdx = 0;
float metL = 0.0f, metR = 0.0f;
juce::Random rng;
juce::Array<int> sustainedNotes;

BIN
b2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB