mirror of
https://codeberg.org/armin/rmx-19.git
synced 2026-10-12 01:02:11 +02:00
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:
parent
d63db3af8a
commit
2e870380d4
17 changed files with 1413 additions and 324 deletions
|
|
@ -54,11 +54,13 @@ message(STATUS "RMX-19 build: ${BUILD_GIT_BRANCH} ${BUILD_GIT_COMMIT}${BUILD_GIT
|
|||
# ---------------------------------------------------------------------------
|
||||
# JUCE
|
||||
# ---------------------------------------------------------------------------
|
||||
if(NOT DEFINED JUCE_ROOT)
|
||||
if(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt")
|
||||
set(JUCE_ROOT "$ENV{HOME}/JUCE")
|
||||
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")
|
||||
message(FATAL_ERROR "JUCE not found. Set -DJUCE_ROOT=<path to JUCE source>")
|
||||
|
|
@ -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)
|
||||
18
Makefile
18
Makefile
|
|
@ -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
269
README.md
|
|
@ -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.
|
||||
|
|
@ -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,62 +561,70 @@ 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 "-----";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -39,8 +39,6 @@ public:
|
|||
SynthEngine& getEngine() { return engine; }
|
||||
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)
|
||||
};
|
||||
|
|
@ -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
121
Source/Probe.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
BIN
b2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Loading…
Add table
Add a link
Reference in a new issue