#include "LcdPanel.h" #include "BuildInfo.h" #include // --------------------------------------------------------------------------- // 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) { 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); } 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) { const int v = proc.getActiveVoices(); if (v != lastVoices) { lastVoices = v; needsRedraw = true; } } 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 int cell = 2; const int w = gridW * cell; const int h = gridH * cell; const int ox = (b.getWidth() - w) / 2; const int oy = (b.getHeight() - h) / 2 + 2; 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); drawContent (dg); } const int W = displayImage.getWidth(); const int H = displayImage.getHeight(); // start from the pre-rendered base layer (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); } // 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 { 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 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::Patch: drawPatchList (g); break; case Screen::Param: drawParamScreen (g); break; case Screen::System: drawSystem (g); break; case Screen::Info: drawInfo (g); break; } } 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 int a = (int) (255.0f * juce::jlimit (0.0f, 1.0f, intensity)); g.setColour (juce::Colour::fromRGBA (255, 255, 255, a)); g.fillRect (x, y, 1, 1); } 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, "* 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) { 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, 1.0f); pxText (g, 14, 3, 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); } void LcdPanel::drawMenu (juce::Graphics& g) { static const char* items[] = { "PATCH", "VOICE", "FILTER", "ENVELOPE", "LFO", "FX", "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::drawPatchList (juce::Graphics& g) { const int bankSize = proc.getBank().size(); 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 * 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 (idx == 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::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 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 = 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 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 ------------------------------------------------------ int LcdPanel::rowCountForScreen() const { switch (screen) { case Screen::Menu: return 8; case Screen::System: return 4; default: return 0; } } int LcdPanel::paramCountForScreen (int idx) const { switch (idx) { case 0: return 4; case 1: return 5; case 2: return 8; case 3: return 4; case 4: return 4; case 5: return 5; 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 "MASTER"; default: return ""; } } void LcdPanel::openParamScreen (int idx) { paramScreenIdx = juce::jlimit (0, 5, idx); cursor = 0; enterScreen (Screen::Param); } void LcdPanel::moveCursor (int d) { if (screen == Screen::Patch) { patchCursor = juce::jlimit (0, proc.getBank().size() - 1, patchCursor + d); setDirty(); } else if (screen == Screen::Param) { cursor = juce::jlimit (0, paramCountForScreen (paramScreenIdx) - 1, cursor + d); setDirty(); } else if (rowCountForScreen() > 0) { cursor = juce::jlimit (0, rowCountForScreen() - 1, cursor + d); setDirty(); } } void LcdPanel::stepDelaySync (int dir) { auto* par = dynamic_cast (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 (0.0, (double) (prm::delaySyncCount - 1)).convertTo0to1 ((double) nv)); setDirty(); } void LcdPanel::stepParam (prm::Id id, int dir, bool coarse) { auto* par = dynamic_cast (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::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 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 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) { proc.selectPatch (patchCursor); setDirty(); } } void LcdPanel::navLeft() { adjustCurrent (-1, false); } void LcdPanel::navRight() { adjustCurrent (1, false); } 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: enterScreen (Screen::Patch); 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: enterScreen (Screen::Info); break; default: break; } break; case Screen::Patch: proc.selectPatch (patchCursor); enterScreen (Screen::Menu); 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::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; }