mirror of
https://codeberg.org/armin/rmx-19.git
synced 2026-10-11 16:51:55 +02:00
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution - evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff wander, resonance growl, warp drive/bias, cross-mod bloom - per-voice unison detune spread for fat stacks - FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay, Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL) - 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval - calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO on filter sustain collapse
1048 lines
43 KiB
C++
1048 lines
43 KiB
C++
#include "LcdPanel.h"
|
|
#include "BuildInfo.h"
|
|
#include <cstring>
|
|
#include <cmath>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5x7 dot-matrix font. Each glyph is 5 columns x 7 rows, bit 6 (0x40) of each
|
|
// row byte is the MSB (leftmost) dot.
|
|
// ---------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
static constexpr int NW = 5, NH = 7;
|
|
|
|
struct Entry { int key; const juce::uint8* rows; };
|
|
|
|
static const juce::uint8 A_[NH] = { 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 };
|
|
static const juce::uint8 B_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110 };
|
|
static const juce::uint8 C_[NH] = { 0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110 };
|
|
static const juce::uint8 D_[NH] = { 0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100 };
|
|
static const juce::uint8 E_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111 };
|
|
static const juce::uint8 F_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000 };
|
|
static const juce::uint8 G_[NH] = { 0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111 };
|
|
static const juce::uint8 H_[NH] = { 0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 };
|
|
static const juce::uint8 I_[NH] = { 0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 };
|
|
static const juce::uint8 J_[NH] = { 0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100 };
|
|
static const juce::uint8 K_[NH] = { 0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 };
|
|
static const juce::uint8 L_[NH] = { 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111 };
|
|
static const juce::uint8 M_[NH] = { 0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001 };
|
|
static const juce::uint8 N_[NH] = { 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001 };
|
|
static const juce::uint8 O_[NH] = { 0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110 };
|
|
static const juce::uint8 P_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000 };
|
|
static const juce::uint8 Q_[NH] = { 0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101 };
|
|
static const juce::uint8 R_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001 };
|
|
static const juce::uint8 S_[NH] = { 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110 };
|
|
static const juce::uint8 T_[NH] = { 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 };
|
|
static const juce::uint8 U_[NH] = { 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110 };
|
|
static const juce::uint8 V_[NH] = { 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100 };
|
|
static const juce::uint8 W_[NH] = { 0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001 };
|
|
static const juce::uint8 X_[NH] = { 0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001 };
|
|
static const juce::uint8 Y_[NH] = { 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100 };
|
|
static const juce::uint8 Z_[NH] = { 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111 };
|
|
static const juce::uint8 n0_[NH] = { 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110 };
|
|
static const juce::uint8 n1_[NH] = { 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 };
|
|
static const juce::uint8 n2_[NH] = { 0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111 };
|
|
static const juce::uint8 n3_[NH] = { 0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110 };
|
|
static const juce::uint8 n4_[NH] = { 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010 };
|
|
static const juce::uint8 n5_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b00001, 0b00001, 0b11110 };
|
|
static const juce::uint8 n6_[NH] = { 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110 };
|
|
static const juce::uint8 n7_[NH] = { 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000 };
|
|
static const juce::uint8 n8_[NH] = { 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110 };
|
|
static const juce::uint8 n9_[NH] = { 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100 };
|
|
static const juce::uint8 sp_[NH] = { 0, 0, 0, 0, 0, 0, 0 };
|
|
static const juce::uint8 ex_[NH] = { 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100 };
|
|
static const juce::uint8 dt_[NH] = { 0, 0, 0, 0, 0, 0b00100, 0b00100 };
|
|
static const juce::uint8 co_[NH] = { 0, 0, 0, 0, 0, 0b00110, 0b00110 };
|
|
static const juce::uint8 cl_[NH] = { 0, 0b00100, 0b00100, 0, 0b00100, 0b00100, 0 };
|
|
static const juce::uint8 mi_[NH] = { 0, 0, 0, 0b11111, 0, 0, 0 };
|
|
static const juce::uint8 pl_[NH] = { 0, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0 };
|
|
static const juce::uint8 sl_[NH] = { 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0, 0 };
|
|
static const juce::uint8 pc_[NH] = { 0b10100, 0b10100, 0b01000, 0b10100, 0b00101, 0b00101, 0b00010 };
|
|
static const juce::uint8 ap_[NH] = { 0b00100, 0b00100, 0b00100, 0, 0, 0, 0 };
|
|
static const juce::uint8 lt_[NH] = { 0b00011, 0b00110, 0b01100, 0b11000, 0b01100, 0b00110, 0b00011 };
|
|
static const juce::uint8 gt_[NH] = { 0b11000, 0b01100, 0b00110, 0b00011, 0b00110, 0b01100, 0b11000 };
|
|
static const juce::uint8 eq_[NH] = { 0, 0, 0b11111, 0, 0b11111, 0, 0 };
|
|
static const juce::uint8 hs_[NH] = { 0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010 };
|
|
|
|
static const Entry table[] =
|
|
{
|
|
{ 'A', A_ }, { 'B', B_ }, { 'C', C_ }, { 'D', D_ }, { 'E', E_ },
|
|
{ 'F', F_ }, { 'G', G_ }, { 'H', H_ }, { 'I', I_ }, { 'J', J_ },
|
|
{ 'K', K_ }, { 'L', L_ }, { 'M', M_ }, { 'N', N_ }, { 'O', O_ },
|
|
{ 'P', P_ }, { 'Q', Q_ }, { 'R', R_ }, { 'S', S_ }, { 'T', T_ },
|
|
{ 'U', U_ }, { 'V', V_ }, { 'W', W_ }, { 'X', X_ }, { 'Y', Y_ },
|
|
{ 'Z', Z_ }, { '0', n0_ }, { '1', n1_ }, { '2', n2_ }, { '3', n3_ },
|
|
{ '4', n4_ }, { '5', n5_ }, { '6', n6_ }, { '7', n7_ }, { '8', n8_ },
|
|
{ '9', n9_ }, { ' ', sp_ }, { '.', dt_ }, { ',', co_ }, { ':', cl_ },
|
|
{ '-', mi_ }, { '+', pl_ }, { '/', sl_ }, { '%', pc_ }, { '\'', ap_ },
|
|
{ '<', lt_ }, { '>', gt_ }, { '=', eq_ }, { '#', hs_ }, { '!', ex_ },
|
|
};
|
|
|
|
} // 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';
|
|
for (const auto& e : table)
|
|
if (e.key == key) return e.rows;
|
|
return sp_;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LcdPanel::LcdPanel (RMX19AudioProcessor& p)
|
|
: proc (p)
|
|
{
|
|
displayImage = juce::Image (juce::Image::ARGB, gridW * subsample, gridH * subsample, false);
|
|
presentImage = juce::Image (juce::Image::ARGB, gridW * presentScale, gridH * presentScale, false);
|
|
backdrop = juce::Image (juce::Image::ARGB, gridW * subsample, gridH * subsample, false);
|
|
buildBackdrop();
|
|
splashUntil = juce::Time::getMillisecondCounter() + 1500;
|
|
lastMidi = proc.getLastMidiTime();
|
|
startTimerHz (22);
|
|
}
|
|
|
|
LcdPanel::~LcdPanel()
|
|
{
|
|
stopTimer();
|
|
}
|
|
|
|
void LcdPanel::timerCallback()
|
|
{
|
|
bool needsRedraw = (screen == Screen::Splash);
|
|
|
|
if (proc.getLastMidiTime() != lastMidi)
|
|
{
|
|
lastMidi = proc.getLastMidiTime();
|
|
powerGlow = 1.0f;
|
|
needsRedraw = true;
|
|
}
|
|
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)
|
|
{
|
|
lastVoices = proc.getActiveVoices();
|
|
needsRedraw = true; // drive the MIDI blip + VU meter
|
|
}
|
|
|
|
if (needsRedraw)
|
|
setDirty();
|
|
}
|
|
|
|
void LcdPanel::paint (juce::Graphics& g)
|
|
{
|
|
const auto b = getLocalBounds();
|
|
|
|
// recessed bezel
|
|
g.setColour (juce::Colour (0xff0c0a08));
|
|
g.fillRect (b);
|
|
g.setColour (juce::Colour (0xff4a453c));
|
|
g.drawRect (b, 2);
|
|
|
|
// inner LCD glass (black)
|
|
const auto rect = lcdImageRect();
|
|
g.setColour (juce::Colour (0xff020703));
|
|
g.fillRect (rect);
|
|
|
|
if (contentDirty)
|
|
{
|
|
rebuild();
|
|
contentDirty = false;
|
|
}
|
|
|
|
g.drawImage (presentImage,
|
|
rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(),
|
|
0, 0, presentImage.getWidth(), presentImage.getHeight());
|
|
}
|
|
|
|
// the on-screen rectangle (component coords) the dot-matrix image occupies:
|
|
// exactly the presentation pixels, centered inside the LCD module (with a small
|
|
// extra downward shift to match the bezel). No scaling is applied so the dot
|
|
// matrix keeps its intended physical size.
|
|
juce::Rectangle<int> LcdPanel::lcdImageRect() const
|
|
{
|
|
const int w = presentImage.getWidth();
|
|
const int h = presentImage.getHeight();
|
|
const auto b = getLocalBounds();
|
|
return { (b.getWidth() - w) / 2, (b.getHeight() - h) / 2 + 2, w, h };
|
|
}
|
|
|
|
void LcdPanel::mouseDown (const juce::MouseEvent& e)
|
|
{
|
|
if (screen != Screen::Home)
|
|
return;
|
|
|
|
// map the click into logical dot-matrix coordinates
|
|
const auto rect = lcdImageRect();
|
|
const float lx = (e.x - rect.getX()) * (float) gridW / (float) rect.getWidth();
|
|
const float ly = (e.y - rect.getY()) * (float) gridH / (float) rect.getHeight();
|
|
|
|
// patch number/name row on the home screen (5x7 font at y=3), shown as "<num>: <name>"
|
|
const auto& cur = proc.getBank().get (proc.getPatchIndex());
|
|
const int nameEnd = 2 + (4 + (int) juce::String (cur.name).length()) * (NW + 1) + 2;
|
|
if (ly < 0.0f || ly >= 11.0f) return;
|
|
if (lx < 0.0f || lx >= (float) nameEnd) return;
|
|
|
|
// native mouse menu: one submenu per category, ticking the loaded patch
|
|
juce::PopupMenu menu;
|
|
const auto& cats = proc.getBank().categories();
|
|
for (size_t c = 0; c < cats.names.size(); ++c)
|
|
{
|
|
juce::PopupMenu sub;
|
|
for (int pi : cats.patchIndices[(size_t) c])
|
|
{
|
|
const bool isCurrent = (pi == proc.getPatchIndex());
|
|
sub.addItem (proc.getBank().get (pi).name, true, isCurrent,
|
|
[this, pi] { proc.selectPatch (pi); setDirty(); });
|
|
}
|
|
menu.addSubMenu (cats.names[(size_t) c], sub);
|
|
}
|
|
|
|
menu.showMenuAsync (juce::PopupMenu::Options().withTargetScreenArea (
|
|
juce::Rectangle<int> (e.getScreenX(), e.getScreenY(), 1, 1)));
|
|
}
|
|
|
|
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;
|
|
const float radius = std::min (W, H) * 2.5f;
|
|
juce::ColourGradient backlight (juce::Colour::fromRGBA (0x40, 0x80, 0x50, 40), cx, cy,
|
|
juce::Colour::fromRGBA (0x20, 0x40, 0x28, 0), cx + radius, cy, true);
|
|
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.
|
|
// Alpha is boosted since the 2:1 downsample to the presentation image halves it.
|
|
for (int y = 0; y < gridH; ++y)
|
|
for (int x = 0; x < gridW; ++x)
|
|
{
|
|
const float gx = (x * (float) subsample) + subsample * 0.5f;
|
|
const float gy = (y * (float) subsample) + subsample * 0.5f;
|
|
const float r = subsample * 0.32f;
|
|
dg.setColour (juce::Colour::fromRGBA (0x00, 0x88, 0x55, 14));
|
|
dg.fillEllipse (gx - r, gy - r, r * 2.0f, r * 2.0f);
|
|
}
|
|
}
|
|
|
|
void LcdPanel::rebuild()
|
|
{
|
|
const int W = displayImage.getWidth();
|
|
const int H = displayImage.getHeight();
|
|
|
|
// start from the pre-rendered 3D glass base (backlight, sheen, vignette, ghost dots)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// paint the content dots directly onto the glass base; px() stamps a rounded,
|
|
// anti-aliased dot for every lit segment
|
|
{
|
|
juce::Graphics dg (displayImage);
|
|
drawContent (dg);
|
|
}
|
|
|
|
// exact integer box downsample (2:1) to the presentation resolution. Because
|
|
// each dot is still drawn large enough to touch its neighbours only at the
|
|
// final 2px/dot image, text reads as crisp, distinct dots.
|
|
const int PW = presentImage.getWidth(), PH = presentImage.getHeight();
|
|
const int kx = W / PW, ky = H / PH, n = kx * ky;
|
|
{
|
|
juce::Image::BitmapData src (displayImage, juce::Image::BitmapData::readOnly);
|
|
juce::Image::BitmapData dst (presentImage, juce::Image::BitmapData::writeOnly);
|
|
for (int py = 0; py < PH; ++py)
|
|
for (int px = 0; px < PW; ++px)
|
|
{
|
|
unsigned r = 0, g = 0, b = 0, a = 0;
|
|
for (int yy = 0; yy < ky; ++yy)
|
|
{
|
|
const juce::PixelARGB* srcRow =
|
|
reinterpret_cast<const juce::PixelARGB*> (src.getPixelPointer (px * kx, py * ky + yy));
|
|
for (int xx = 0; xx < kx; ++xx)
|
|
{
|
|
const juce::PixelARGB p = srcRow[xx];
|
|
r += p.getRed(); g += p.getGreen();
|
|
b += p.getBlue(); a += p.getAlpha();
|
|
}
|
|
}
|
|
juce::PixelARGB* dstRow =
|
|
reinterpret_cast<juce::PixelARGB*> (dst.getPixelPointer (px, py));
|
|
*dstRow = juce::PixelARGB ((juce::uint8) (a / n), (juce::uint8) (r / n),
|
|
(juce::uint8) (g / n), (juce::uint8) (b / n));
|
|
}
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawContent (juce::Graphics& g)
|
|
{
|
|
switch (screen)
|
|
{
|
|
case Screen::Splash: drawSplash (g); break;
|
|
case Screen::Home: drawHome (g); break;
|
|
case Screen::Menu: drawMenu (g); break;
|
|
case Screen::Patches: drawPatches (g); break;
|
|
case Screen::Patch: drawPatchList (g); break;
|
|
case Screen::Param: drawParamScreen (g); break;
|
|
case Screen::System: drawSystem (g); break;
|
|
case Screen::Info: drawInfo (g); break;
|
|
case Screen::Category: drawCategory (g); break; // legacy
|
|
}
|
|
}
|
|
|
|
// Stamp one lit segment as a solid full-cell dot at subsample resolution.
|
|
// Drawing the whole 4x4 subsample cell solid (instead of an anti-aliased circle)
|
|
// makes the 2:1 box-downsample produce a perfectly sharp 2px/dot block at full
|
|
// brightness. An AA circle here lands centred exactly on the corner of the four
|
|
// output pixels and gets smeared to ~half brightness — that read as soft text.
|
|
void LcdPanel::px (juce::Graphics& g, int x, int y, float intensity)
|
|
{
|
|
if (x < 0 || y < 0 || x >= gridW || y >= gridH) return;
|
|
if (intensity <= 0.0f) return;
|
|
const float s = (float) subsample;
|
|
g.setColour (juce::Colour::fromRGBA (
|
|
0xa0, 0xff, 0xc0, (juce::uint8) (255.0f * juce::jlimit (0.0f, 1.0f, intensity))));
|
|
g.fillRect (x * s, y * s, s, s);
|
|
}
|
|
|
|
void LcdPanel::pxText (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 = glyphFor (key);
|
|
for (int row = 0; row < NH; ++row)
|
|
{
|
|
const juce::uint8 bits = rows[row];
|
|
for (int col = 0; col < NW; ++col)
|
|
if (bits & (1u << (NW - 1 - col)))
|
|
px (g, cx + col, y + row, intensity);
|
|
}
|
|
cx += NW + 1;
|
|
}
|
|
}
|
|
|
|
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)
|
|
for (int xx = 0; xx < w; ++xx)
|
|
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));
|
|
if (filled > 0) drawFilled (g, x, y, filled, 1, intensity);
|
|
}
|
|
|
|
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);
|
|
|
|
drawFilled (g, 2, 57, gridW - 4, 1, 0.35f);
|
|
pxTextM (g, 4, 58, git, 0.75f);
|
|
|
|
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)
|
|
{
|
|
pxTextM (g, 58, 8, "* WELCOME TO RMX-19 *", 1.0f);
|
|
pxTextM (g, 28, 20, "* SUPER SOUND MODULE *", 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)
|
|
{
|
|
const int patch = proc.getPatchIndex();
|
|
const auto& p = proc.getBank().get (patch);
|
|
|
|
const juce::String num = juce::String (patch + 1).paddedLeft ('0', 2);
|
|
pxText (g, 2, 3, num + ": " + juce::String (p.name), 1.0f);
|
|
pxTextM (g, 4, 17, p.category, 0.9f);
|
|
|
|
drawFilled (g, 4, 28, 88, 1, 0.5f);
|
|
drawHBar (g, 4, 29, 88, proc.getActiveVoices() / 16.0, 0.9f);
|
|
|
|
// arp indicator on home
|
|
if (auto* arpPar = proc.apvts.getRawParameterValue ("arpon"))
|
|
{
|
|
if (arpPar->load() >= 0.5f)
|
|
{
|
|
const int rateIdx = (int) proc.apvts.getRawParameterValue ("arprate")->load();
|
|
const int patIdx = (int) proc.apvts.getRawParameterValue ("arppattern")->load();
|
|
const juce::String arpTxt = "ARP " + juce::String (prm::arpRateName (rateIdx))
|
|
+ " " + juce::String (prm::arpPatternName (patIdx));
|
|
pxTextM (g, 96, 17, arpTxt, 0.85f);
|
|
}
|
|
}
|
|
|
|
// bottom status strip: MIDI input blip + output VU meter
|
|
pxTextM (g, 4, 58, "MIDI IN", 0.6f);
|
|
drawFilled (g, 35, 60, 2, 3, juce::jmax (0.15f, powerGlow));
|
|
|
|
pxTextM (g, 50, 58, "OUT", 0.6f);
|
|
const int vw = 84;
|
|
const int vx = 68;
|
|
const int filled = juce::roundToInt (vw * juce::jlimit (0.0f, 1.0f, proc.getOutputLevel()));
|
|
if (filled > 0)
|
|
{
|
|
const int warm = juce::jmin (filled, vw / 2);
|
|
drawFilled (g, vx, 60, warm, 2, 0.65f);
|
|
drawFilled (g, vx + warm, 60, filled - warm, 2, 1.0f);
|
|
drawFilled (g, vx + filled - 1, 62, 1, 1, 0.4f); // leading-edge tick
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawMenu (juce::Graphics& g)
|
|
{
|
|
static const char* items[] = {
|
|
"PATCHES", "VOICE", "FILTER", "ENVELOPE", "LFO", "FX", "ARP", "MASTER", "ABOUT"
|
|
};
|
|
const int rows = (int) juce::numElementsInArray (items);
|
|
for (int i = 0; i < rows; ++i)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawCategory (juce::Graphics& g)
|
|
{
|
|
const auto& cats = proc.getBank().categories();
|
|
const int count = (int) cats.names.size() + 1; // +1 = SETTINGS row
|
|
const int visible = 8;
|
|
const int top = juce::jmax (0, juce::jmin (catCursor - 3, count - visible));
|
|
for (int i = 0; i < visible; ++i)
|
|
{
|
|
const int idx = top + i;
|
|
if (idx >= count) break;
|
|
const int y = 2 + i * 7;
|
|
const bool sel = (idx == catCursor);
|
|
if (sel) drawSelBar (g, y - 1);
|
|
if (idx == (int) cats.names.size())
|
|
{
|
|
pxTextM (g, 6, y, "SETTINGS", sel ? 1.0f : 0.72f);
|
|
drawFilled (g, 6, y - 1, gridW - 12, 1, 0.30f);
|
|
continue;
|
|
}
|
|
pxTextM (g, 6, y, cats.names[(size_t) idx], sel ? 1.0f : 0.72f);
|
|
const int pcount = (int) cats.patchIndices[(size_t) idx].size();
|
|
pxTextM (g, gridW - 16, y, juce::String (pcount), sel ? 0.85f : 0.55f);
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawPatches (juce::Graphics& g)
|
|
{
|
|
const auto& cats = proc.getBank().categories();
|
|
const int count = (int) cats.names.size();
|
|
const int visible = 8;
|
|
const int top = juce::jmax (0, juce::jmin (catCursor - 3, count - visible));
|
|
for (int i = 0; i < visible; ++i)
|
|
{
|
|
const int idx = top + i;
|
|
if (idx >= count) break;
|
|
const int y = 2 + i * 7;
|
|
const bool sel = (idx == catCursor);
|
|
if (sel) drawSelBar (g, y - 1);
|
|
pxTextM (g, 6, y, cats.names[(size_t) idx], sel ? 1.0f : 0.72f);
|
|
const int pcount = (int) cats.patchIndices[(size_t) idx].size();
|
|
pxTextM (g, gridW - 16, y, juce::String (pcount), sel ? 0.85f : 0.55f);
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawPatchList (juce::Graphics& g)
|
|
{
|
|
const int count = patchCountForCurrentView();
|
|
const int visible = 8;
|
|
const int top = juce::jmax (0, juce::jmin (patchCursor - 3, count - visible));
|
|
for (int i = 0; i < visible; ++i)
|
|
{
|
|
const int idx = top + i;
|
|
if (idx >= count) break;
|
|
const int pi = patchIndexAtCursor (idx);
|
|
const auto& p = proc.getBank().get (pi);
|
|
const int y = 2 + i * 7;
|
|
const bool sel = (idx == patchCursor);
|
|
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 (pi == proc.getPatchIndex())
|
|
pxTextM (g, gridW - 7, y, "*", sel ? 1.0f : 0.9f);
|
|
}
|
|
}
|
|
|
|
void LcdPanel::drawParamScreen (juce::Graphics& g)
|
|
{
|
|
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::detune1, prm::Id::detune, prm::Id::oscMix,
|
|
prm::Id::xmod, prm::Id::xmix, prm::Id::evolve, prm::Id::warp };
|
|
|
|
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
|
|
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 arp[] = { prm::Id::arpOn, prm::Id::arpRate, prm::Id::arpPattern, prm::Id::arpOct, prm::Id::arpGate };
|
|
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;
|
|
switch (paramScreenIdx)
|
|
{
|
|
case 0: rows = voice; count = (int) juce::numElementsInArray (voice); break;
|
|
case 1: rows = filter; count = (int) juce::numElementsInArray (filter); 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 = arp; count = (int) juce::numElementsInArray (arp); break;
|
|
case 6: rows = master; count = (int) juce::numElementsInArray (master); break;
|
|
default: return;
|
|
}
|
|
|
|
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();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 * 7;
|
|
const bool sel = (i == cursor);
|
|
if (sel) drawSelBar (g, y - 1);
|
|
pxTextM (g, 6, y, items[i], sel ? 1.0f : 0.72f);
|
|
}
|
|
if (cursor == 2 && powerGlow > 0.5f)
|
|
pxTextM (g, 6, 50, "MIDI RX " + juce::String (lastMidi ? "ACTIVE" : "IDLE"), 0.6f);
|
|
}
|
|
|
|
void LcdPanel::drawInfo (juce::Graphics& g)
|
|
{
|
|
pxTextM (g, 3, 3, "RMX-19 SYNTH MODULE 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 ------------------------------------------------------
|
|
|
|
int LcdPanel::rowCountForScreen() const
|
|
{
|
|
switch (screen)
|
|
{
|
|
case Screen::Menu: return 9;
|
|
case Screen::Patches: return proc.getBank().categories().names.size();
|
|
case Screen::System: return 4;
|
|
case Screen::Category: return proc.getBank().categories().names.size() + 1; // +1 = SETTINGS row
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
int LcdPanel::paramCountForScreen (int idx) const
|
|
{
|
|
switch (idx)
|
|
{
|
|
case 0: return 5;
|
|
case 1: return 5;
|
|
case 2: return 8;
|
|
case 3: return 4;
|
|
case 4: return 4; // 3 FX params + SYNC row
|
|
case 5: return 5; // ARP: on, rate, pattern, oct, gate
|
|
case 6: return 6;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
const char* LcdPanel::paramScreenTitle (int idx) const
|
|
{
|
|
switch (idx)
|
|
{
|
|
case 0: return "VOICE";
|
|
case 1: return "FILTER";
|
|
case 2: return "ENVELOPE";
|
|
case 3: return "LFO";
|
|
case 4: return "FX";
|
|
case 5: return "ARP";
|
|
case 6: return "MASTER";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
void LcdPanel::openParamScreen (int idx)
|
|
{
|
|
paramScreenIdx = juce::jlimit (0, 6, idx);
|
|
cursor = 0;
|
|
enterScreen (Screen::Param);
|
|
}
|
|
|
|
// cyclic cursor step: wrap around the list instead of clamping at the ends
|
|
static int cyclicStep (int cur, int count, int dir)
|
|
{
|
|
if (count <= 0) return 0;
|
|
int n = (cur + dir) % count;
|
|
if (n < 0) n += count;
|
|
return n;
|
|
}
|
|
|
|
void LcdPanel::moveCursor (int d)
|
|
{
|
|
if (screen == Screen::Patches)
|
|
{
|
|
const int count = proc.getBank().categories().names.size();
|
|
catCursor = cyclicStep (catCursor, count, d);
|
|
setDirty();
|
|
}
|
|
else if (screen == Screen::Category)
|
|
{
|
|
const int count = proc.getBank().categories().names.size() + 1; // +1 = SETTINGS row
|
|
catCursor = cyclicStep (catCursor, count, d);
|
|
setDirty();
|
|
}
|
|
else if (screen == Screen::Patch)
|
|
{
|
|
const int count = patchCountForCurrentView();
|
|
patchCursor = cyclicStep (patchCursor, count, d);
|
|
setDirty();
|
|
}
|
|
else if (screen == Screen::Param)
|
|
{
|
|
cursor = cyclicStep (cursor, paramCountForScreen (paramScreenIdx), d);
|
|
setDirty();
|
|
}
|
|
else if (rowCountForScreen() > 0)
|
|
{
|
|
cursor = cyclicStep (cursor, rowCountForScreen(), d);
|
|
setDirty();
|
|
}
|
|
}
|
|
|
|
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));
|
|
if (par == nullptr) return;
|
|
|
|
const float cur = proc.apvts.getRawParameterValue (prm::desc (id).id)->load();
|
|
const float nv = prm::changeParam (id, cur, dir, coarse);
|
|
if (nv != cur)
|
|
par->setValueNotifyingHost (par->convertTo0to1 (nv));
|
|
setDirty();
|
|
}
|
|
|
|
static prm::Id paramForCursor (int screenIdx, int row)
|
|
{
|
|
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune1, 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 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 arp[] = { prm::Id::arpOn, prm::Id::arpRate, prm::Id::arpPattern, prm::Id::arpOct, prm::Id::arpGate };
|
|
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 envMt[row];
|
|
case 3: return lfo[row];
|
|
case 4: return fx[row];
|
|
case 5: return arp[row];
|
|
case 6: return master[row];
|
|
default: return prm::Id::vol;
|
|
}
|
|
}
|
|
|
|
void LcdPanel::adjustCurrent (int dir, bool coarse)
|
|
{
|
|
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::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)
|
|
{
|
|
const int pi = patchIndexAtCursor (patchCursor);
|
|
if (pi >= 0 && pi < proc.getBank().size())
|
|
proc.selectPatch (pi);
|
|
setDirty();
|
|
}
|
|
}
|
|
void LcdPanel::navLeft() { adjustCurrent (-1, false); }
|
|
void LcdPanel::navRight() { adjustCurrent (1, false); }
|
|
|
|
void LcdPanel::openPatchBrowser()
|
|
{
|
|
const auto& cats = proc.getBank().categories();
|
|
int start = 0;
|
|
const int cur = proc.getPatchIndex();
|
|
for (size_t c = 0; c < cats.patchIndices.size(); ++c)
|
|
{
|
|
bool found = false;
|
|
for (int pi : cats.patchIndices[(size_t) c])
|
|
if (pi == cur) { found = true; break; }
|
|
if (found) { start = (int) c; break; }
|
|
}
|
|
catCursor = start;
|
|
enterScreen (Screen::Patches);
|
|
}
|
|
|
|
void LcdPanel::navEnter()
|
|
{
|
|
switch (screen)
|
|
{
|
|
case Screen::Splash: enterScreen (Screen::Home); break;
|
|
case Screen::Home:
|
|
enterScreen (Screen::Menu);
|
|
break;
|
|
case Screen::Menu:
|
|
switch (cursor)
|
|
{
|
|
case 0: openPatchBrowser(); break;
|
|
case 1: openParamScreen (0); break;
|
|
case 2: openParamScreen (1); break;
|
|
case 3: openParamScreen (2); break;
|
|
case 4: openParamScreen (3); break;
|
|
case 5: openParamScreen (4); break;
|
|
case 6: openParamScreen (5); break;
|
|
case 7: openParamScreen (6); break;
|
|
case 8: enterScreen (Screen::Info); break;
|
|
default: break;
|
|
}
|
|
break;
|
|
case Screen::Patches:
|
|
{
|
|
const auto& cats = proc.getBank().categories();
|
|
if (catCursor >= 0 && catCursor < (int) cats.names.size())
|
|
{
|
|
buildFilteredPatches (catCursor);
|
|
patchCursor = 0;
|
|
enterScreen (Screen::Patch);
|
|
}
|
|
break;
|
|
}
|
|
case Screen::Category:
|
|
{
|
|
const auto& cats = proc.getBank().categories();
|
|
if (catCursor >= 0 && catCursor < (int) cats.names.size())
|
|
{
|
|
buildFilteredPatches (catCursor);
|
|
patchCursor = 0;
|
|
enterScreen (Screen::Patch);
|
|
}
|
|
else if (catCursor == (int) cats.names.size())
|
|
{
|
|
enterScreen (Screen::Menu); // SETTINGS row -> main menu
|
|
}
|
|
break;
|
|
}
|
|
case Screen::Patch:
|
|
proc.selectPatch (patchIndexAtCursor (patchCursor));
|
|
enterScreen (Screen::Home); // back to the main screen showing the new patch
|
|
break;
|
|
case Screen::System:
|
|
switch (cursor)
|
|
{
|
|
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::Menu); break;
|
|
default: break;
|
|
}
|
|
break;
|
|
case Screen::Param:
|
|
break; // stay in the submenu; left/right edits the highlighted param
|
|
case Screen::Info:
|
|
enterScreen (Screen::Menu);
|
|
break;
|
|
default:
|
|
enterScreen (Screen::Menu);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void LcdPanel::navExit()
|
|
{
|
|
if (screen == Screen::Patch)
|
|
enterScreen (Screen::Patches);
|
|
else if (screen == Screen::Patches)
|
|
enterScreen (Screen::Menu);
|
|
else if (screen == Screen::Category)
|
|
enterScreen (Screen::Menu); // legacy: go to main menu
|
|
else if (screen == Screen::Menu)
|
|
enterScreen (Screen::Home);
|
|
else if (screen != Screen::Home)
|
|
enterScreen (Screen::Menu);
|
|
}
|
|
|
|
bool LcdPanel::keyTrap (const juce::KeyPress& k)
|
|
{
|
|
if (k.isKeyCode (juce::KeyPress::upKey)) { navUp(); return true; }
|
|
if (k.isKeyCode (juce::KeyPress::downKey)) { navDown(); return true; }
|
|
if (k.isKeyCode (juce::KeyPress::leftKey)) { navLeft(); return true; }
|
|
if (k.isKeyCode (juce::KeyPress::rightKey)) { navRight(); return true; }
|
|
if (k.isKeyCode (juce::KeyPress::returnKey) || k.getTextCharacter() == ' ')
|
|
{ navEnter(); return true; }
|
|
if (k.isKeyCode (juce::KeyPress::escapeKey))
|
|
{ navExit(); return true; }
|
|
return false;
|
|
}
|
|
|
|
int LcdPanel::patchCountForCurrentView() const
|
|
{
|
|
if (! filteredPatches.empty())
|
|
return (int) filteredPatches.size();
|
|
return proc.getBank().size();
|
|
}
|
|
|
|
int LcdPanel::patchIndexAtCursor (int viewIdx) const
|
|
{
|
|
if (viewIdx < 0) return 0;
|
|
if (! filteredPatches.empty())
|
|
{
|
|
if (viewIdx < (int) filteredPatches.size())
|
|
return filteredPatches[(size_t) viewIdx];
|
|
return filteredPatches.empty() ? 0 : filteredPatches.back();
|
|
}
|
|
return viewIdx;
|
|
}
|
|
|
|
void LcdPanel::buildFilteredPatches (int catIdx)
|
|
{
|
|
filteredPatches.clear();
|
|
const auto& cats = proc.getBank().categories();
|
|
if (catIdx >= 0 && catIdx < cats.names.size())
|
|
filteredPatches = cats.patchIndices[(size_t) catIdx];
|
|
}
|