#include "PluginEditor.h" #include "BuildInfo_generated.h" static juce::String padRight (const juce::String& s, int width) { if (s.length() >= width) return s.substring (0, width); return s + juce::String::repeatedString (" ", width - s.length()); } SM26AudioProcessorEditor::SM26AudioProcessorEditor (SM26AudioProcessor& p) : AudioProcessorEditor (p), proc (p) { setSize (920, 540); setResizable (true, false); setResizeLimits (720, 400, 1400, 900); addAndMakeVisible (lcd); setupKnobs(); setupNavButtons(); footer.setText (buildGitFooter(), juce::dontSendNotification); footer.setJustificationType (juce::Justification::centredLeft); footer.setColour (juce::Label::textColourId, juce::Colour (0xFF90B080)); footer.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 11.0f, juce::Font::plain })); addAndMakeVisible (footer); buildRootMenu(); startTimerHz (20); } SM26AudioProcessorEditor::~SM26AudioProcessorEditor() { stopTimer(); } juce::String SM26AudioProcessorEditor::buildGitFooter() { return juce::String ("SM-26 v") + SM26_GIT_VERSION + " | branch " + SM26_GIT_BRANCH + " | built " + SM26_BUILD_TIME + " | arm64"; } void SM26AudioProcessorEditor::setupKnobs() { for (int i = 0; i < 4; ++i) { knobSliders[i].setSliderStyle (juce::Slider::RotaryVerticalDrag); knobSliders[i].setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); knobSliders[i].setRange (0.0, 1.0, 0.001); knobSliders[i].setColour (juce::Slider::rotarySliderFillColourId, juce::Colour (0xFF3A8A28)); knobSliders[i].setColour (juce::Slider::rotarySliderOutlineColourId, juce::Colour (0xFF1A2E0A)); knobSliders[i].setColour (juce::Slider::thumbColourId, juce::Colours::transparentBlack); knobSliders[i].addListener (this); addAndMakeVisible (knobSliders[i]); knobLabels[i].setJustificationType (juce::Justification::centred); knobLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFF90B080)); knobLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 11.0f, juce::Font::plain })); addAndMakeVisible (knobLabels[i]); knobValueLabels[i].setJustificationType (juce::Justification::centred); knobValueLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFFC0E0A0)); knobValueLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 12.0f, juce::Font::bold })); addAndMakeVisible (knobValueLabels[i]); knobSliders[i].setValue (0.5, juce::dontSendNotification); } } juce::Font SM26AudioProcessorEditor::NavButtonLookAndFeel::getTextButtonFont (juce::TextButton&, int) { return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 10.5f, juce::Font::bold }); } void SM26AudioProcessorEditor::setupNavButtons() { auto setup = [this](juce::TextButton& btn, juce::Colour bg) { btn.setLookAndFeel (&navLookAndFeel); btn.setColour (juce::TextButton::buttonColourId, bg); btn.setColour (juce::TextButton::textColourOffId, juce::Colour (0xFFE0E0C0)); btn.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A4A18)); addAndMakeVisible (btn); }; setup (navUp, juce::Colour (0xFF2A4A18)); setup (navDown, juce::Colour (0xFF2A4A18)); setup (navLeft, juce::Colour (0xFF2A4A18)); setup (navRight, juce::Colour (0xFF2A4A18)); setup (navEnter, juce::Colour (0xFF3A6A28)); setup (navEsc, juce::Colour (0xFF3A2A18)); navUp.onClick = [this] { if (menu.isOpen()) menu.up(); else cyclePreset (-1); }; navDown.onClick = [this] { if (menu.isOpen()) menu.down(); else cyclePreset (+1); }; navLeft.onClick = [this] { menu.left(); }; navRight.onClick = [this] { menu.right(); }; navEnter.onClick = [this] { menu.open(); }; navEsc.onClick = [this] { if (menu.isOpen()) menu.escape(); else menu.close(); }; } void SM26AudioProcessorEditor::cyclePreset (int delta) { const int count = sm26::PresetStore::count(); if (count <= 0) return; const int idx = proc.engine.presetIndex(); proc.setPresetByIndex ((idx + delta + count) % count); } void SM26AudioProcessorEditor::buildRootMenu() { auto builder = [this]() -> MenuScreen { MenuScreen s; s.title = "SM-26 MAIN MENU"; s.selected = 0; auto addPreset = [&](const char* name, int idx) -> MenuItem { MenuItem item; item.label = name; item.kind = MenuItem::Action; item.onEnter = [this, idx]() { proc.setPresetByIndex (idx); }; return item; }; { MenuItem m; m.label = "PRESETS (" + juce::String (sm26::PresetStore::count()) + ")"; m.kind = MenuItem::Submenu; m.onEnter = [this, addPreset]() { MenuScreen ps; ps.title = "SELECT PRESET"; ps.selected = proc.engine.presetIndex(); for (int i = 0; i < sm26::PresetStore::count(); ++i) ps.items.push_back (addPreset ( sm26::PresetStore::get(i)->name, i)); menu.pushScreen (ps); }; s.items.push_back (m); } s.items.push_back (MenuItem().sep()); { MenuItem m; m.label = "OSCILLATORS"; m.kind = MenuItem::Submenu; m.onEnter = [this]() { MenuScreen os; os.title = "OSCILLATORS"; os.selected = 0; auto addItem = [&](const char* n, int pid) { MenuItem mi; mi.label = n; mi.kind = MenuItem::Value; mi.paramId = pid; mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); }; mi.onAdjust = [this, pid](float d) { float cur = proc.engine.getParam (pid); proc.engine.setParam (pid, sm26::clamp01 (cur + d)); for (int k = 0; k < 4; ++k) { auto kv = proc.engine.knobView (k); if (kv.param == pid) proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f))); } }; os.items.push_back (mi); }; addItem ("OSC1 WAVE", sm26::PO1WAVE); addItem ("OSC2 WAVE", sm26::PO2WAVE); addItem ("O1 OCTAVE", sm26::PO1OCT); addItem ("O1 DETUNE", sm26::PO1CT); addItem ("O2 OCTAVE", sm26::PO2OCT); addItem ("O2 DETUNE", sm26::PO2CT); addItem ("MIX", sm26::PMIX); addItem ("NOISE", sm26::PNOISE); menu.pushScreen (os); }; s.items.push_back (m); } { MenuItem m; m.label = "FILTER"; m.kind = MenuItem::Submenu; m.onEnter = [this]() { MenuScreen fs; fs.title = "FILTER"; fs.selected = 0; auto addItem = [&](const char* n, int pid) { MenuItem mi; mi.label = n; mi.kind = MenuItem::Value; mi.paramId = pid; mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); }; mi.onAdjust = [this, pid](float d) { float cur = proc.engine.getParam (pid); proc.engine.setParam (pid, sm26::clamp01 (cur + d)); for (int k = 0; k < 4; ++k) { auto kv = proc.engine.knobView (k); if (kv.param == pid) proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f))); } }; fs.items.push_back (mi); }; addItem ("CUTOFF", sm26::PCUTOFF); addItem ("RES", sm26::PRES); addItem ("F-ENV", sm26::PFENV); addItem ("F-TYPE", sm26::PFTYPE); menu.pushScreen (fs); }; s.items.push_back (m); } { MenuItem m; m.label = "ENVELOPE"; m.kind = MenuItem::Submenu; m.onEnter = [this]() { MenuScreen es; es.title = "ENVELOPE"; es.selected = 0; auto addItem = [&](const char* n, int pid) { MenuItem mi; mi.label = n; mi.kind = MenuItem::Value; mi.paramId = pid; mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); }; mi.onAdjust = [this, pid](float d) { float cur = proc.engine.getParam (pid); proc.engine.setParam (pid, sm26::clamp01 (cur + d)); for (int k = 0; k < 4; ++k) { auto kv = proc.engine.knobView (k); if (kv.param == pid) proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f))); } }; es.items.push_back (mi); }; addItem ("ATTACK", sm26::PATK); addItem ("DECAY", sm26::PDEC); addItem ("SUSTAIN", sm26::PSUS); addItem ("RELEASE", sm26::PREL); menu.pushScreen (es); }; s.items.push_back (m); } { MenuItem m; m.label = "DELAY"; m.kind = MenuItem::Submenu; m.onEnter = [this]() { MenuScreen ds; ds.title = "DELAY"; ds.selected = 0; auto addItem = [&](const char* n, int pid) { MenuItem mi; mi.label = n; mi.kind = MenuItem::Value; mi.paramId = pid; mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); }; mi.onAdjust = [this, pid](float d) { float cur = proc.engine.getParam (pid); proc.engine.setParam (pid, sm26::clamp01 (cur + d)); for (int k = 0; k < 4; ++k) { auto kv = proc.engine.knobView (k); if (kv.param == pid) proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f))); } }; ds.items.push_back (mi); }; addItem ("TIME/STEPS", sm26::PDTIME); addItem ("STEPS", sm26::PDSTEPS); addItem ("SYNC", sm26::PDSYNC); addItem ("FEEDBACK", sm26::PDFB); addItem ("MIX", sm26::PDMIX); menu.pushScreen (ds); }; s.items.push_back (m); } { MenuItem m; m.label = "SYSTEM"; m.kind = MenuItem::Submenu; m.onEnter = [this]() { MenuScreen ss; ss.title = "SYSTEM"; ss.selected = 0; auto addItem = [&](const char* n, int pid) { MenuItem mi; mi.label = n; mi.kind = MenuItem::Value; mi.paramId = pid; mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); }; mi.onAdjust = [this, pid](float d) { float cur = proc.engine.getParam (pid); proc.engine.setParam (pid, sm26::clamp01 (cur + d)); for (int k = 0; k < 4; ++k) { auto kv = proc.engine.knobView (k); if (kv.param == pid) proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f))); } }; ss.items.push_back (mi); }; addItem ("DRIVE", sm26::PDRIVE); addItem ("GLIDE", sm26::PGLIDE); addItem ("MASTER", sm26::PMASTER); { MenuItem mi; mi.label = "---"; mi.kind = MenuItem::Separator; ss.items.push_back (mi); } { MenuItem mi; mi.label = "ABOUT"; mi.kind = MenuItem::Action; mi.onEnter = [this]() { MenuScreen as; as.title = "ABOUT"; as.selected = 0; { MenuItem m2; m2.label = "SM-26 Synthesizer"; m2.kind = MenuItem::Action; as.items.push_back (m2); } { MenuItem m2; m2.label = "Version: " + juce::String (SM26_GIT_VERSION); m2.kind = MenuItem::Action; as.items.push_back (m2); } { MenuItem m2; m2.label = "Built: " + juce::String (SM26_BUILD_TIME); m2.kind = MenuItem::Action; as.items.push_back (m2); } { MenuItem m2; m2.label = "Branch: " + juce::String (SM26_GIT_BRANCH); m2.kind = MenuItem::Action; as.items.push_back (m2); } { MenuItem m2; m2.label = ""; m2.kind = MenuItem::Separator; as.items.push_back (m2); } { MenuItem m2; m2.label = "52 presets | VST3 + AU"; m2.kind = MenuItem::Action; as.items.push_back (m2); } menu.pushScreen (as); }; ss.items.push_back (mi); } menu.pushScreen (ss); }; s.items.push_back (m); } return s; }; menu.setRootScreenBuilder (builder); } void SM26AudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (juce::Colour (0xFF0A0F06)); const float totalW = (float) getWidth(); const float totalH = (float) getHeight(); g.setColour (juce::Colour (0xFF1A2E0A)); g.fillRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f); g.setColour (juce::Colour (0xFF2A4A18)); g.drawRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f, 1.5f); if (! menu.isOpen()) { g.setColour (juce::Colour (0xFF6AE050)); g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 14.0f, juce::Font::bold })); g.drawText ("SM-26", 20.0f, 16.0f, 100.0f, 22.0f, juce::Justification::centredLeft); } else { g.setColour (juce::Colour (0xFFE0E0C0)); g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 14.0f, juce::Font::bold })); g.drawText (menu.current().title, 20.0f, 16.0f, getWidth() - 40.0f, 22.0f, juce::Justification::centred); } if (menu.isOpen()) { const auto& ms = menu.current(); float y = 44.0f; float itemH = 18.0f; float menuLeft = 20.0f; float menuW = getWidth() - 40.0f; for (int i = 0; i < (int) ms.items.size() && y < (float) getHeight() - 60.0f; ++i) { const auto& item = ms.items[i]; if (item.kind == MenuItem::Separator) { g.setColour (juce::Colour (0xFF2A4A18)); g.drawHorizontalLine ((int) y, menuLeft, menuLeft + menuW); y += 4.0f; continue; } bool selected = (i == ms.selected); if (selected) { g.setColour (juce::Colour (0xFF3A6A28)); g.fillRoundedRectangle (menuLeft, y, menuW, itemH, 3.0f); } g.setColour (selected ? juce::Colour (0xFFE0E0C0) : juce::Colour (0xFF709060)); g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 12.0f, juce::Font::plain })); g.drawText (item.label, menuLeft + 8.0f, y, menuW * 0.6f, itemH, juce::Justification::centredLeft); if (item.kind == MenuItem::Value && item.valueText) { g.setColour (juce::Colour (0xFFA0E080)); g.drawText (item.valueText(), menuLeft + menuW * 0.55f, y, menuW * 0.4f, itemH, juce::Justification::centredRight); } else if (item.kind == MenuItem::Submenu) { g.setColour (juce::Colour (0xFF90B080)); g.drawText (">", menuLeft + menuW - 16.0f, y, 16.0f, itemH, juce::Justification::centredRight); } y += itemH + 1.0f; } } } void SM26AudioProcessorEditor::resized() { const float totalW = (float) getWidth(); const float totalH = (float) getHeight(); float knobSize = 62.0f; float knobGap = 18.0f; float knobsW = 4.0f * knobSize + 3.0f * knobGap; float knobsX = (totalW - knobsW) / 2.0f; float knobsY = juce::jmax (190.0f, totalH - 265.0f); for (int i = 0; i < 4; ++i) { float x = knobsX + (float) i * (knobSize + knobGap); knobSliders[i].setBounds ((int) x, (int) knobsY, (int) knobSize, (int) knobSize); knobLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 2), (int) knobSize, 16); knobValueLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 18), (int) knobSize, 14); } float navSize = 30.0f; float navGap = 3.0f; float navCenterX = totalW / 2.0f; float navCenterY = totalH - 90.0f; navUp.setBounds ((int) (navCenterX - navSize / 2), (int) (navCenterY - navSize - navGap), (int) navSize, (int) navSize); navDown.setBounds ((int) (navCenterX - navSize / 2), (int) (navCenterY + navGap), (int) navSize, (int) navSize); navLeft.setBounds ((int) (navCenterX - navSize - navGap - navSize / 2), (int) (navCenterY - navSize / 2), (int) navSize, (int) navSize); navRight.setBounds ((int) (navCenterX + navGap + navSize / 2), (int) (navCenterY - navSize / 2), (int) navSize, (int) navSize); navEnter.setBounds ((int) (navCenterX - navSize - navGap), (int) (navCenterY + navSize + navGap), (int) navSize, (int) navSize); navEsc.setBounds ((int) (navCenterX + navGap), (int) (navCenterY + navSize + navGap), (int) navSize, (int) navSize); lcd.setBounds (14, 42, (int) totalW - 28, 150); footer.setBounds (8, (int) totalH - 22, (int) totalW - 16, 16); } void SM26AudioProcessorEditor::timerCallback() { updateLCD(); for (int i = 0; i < 4; ++i) { auto kv = proc.engine.knobView (i); float val = proc.engine.getParam (kv.param); if (! knobDragging[i]) { float pos = sm26::clamp01 ((val - kv.lo) / (kv.hi - kv.lo + 1e-9f)); knobSliders[i].setValue (pos, juce::dontSendNotification); } knobLabels[i].setText (kv.name, juce::dontSendNotification); knobValueLabels[i].setText (sm26::formatParamReal (kv.param, val), juce::dontSendNotification); } if (! menu.isOpen() && menuWasOpen) buildRootMenu(); menuWasOpen = menu.isOpen(); repaint(); } void SM26AudioProcessorEditor::sliderValueChanged (juce::Slider* s) { for (int i = 0; i < 4; ++i) { if (s == &knobSliders[i]) { float v = (float) s->getValue(); auto kv = proc.engine.knobView (i); float paramVal = sm26::clamp01 (kv.lo + v * (kv.hi - kv.lo)); proc.engine.setParam (kv.param, paramVal); knobValueLabels[i].setText (sm26::formatParamReal (kv.param, paramVal), juce::dontSendNotification); break; } } } void SM26AudioProcessorEditor::sliderDragStarted (juce::Slider* s) { for (int i = 0; i < 4; ++i) if (s == &knobSliders[i]) knobDragging[i] = true; } void SM26AudioProcessorEditor::sliderDragEnded (juce::Slider* s) { for (int i = 0; i < 4; ++i) if (s == &knobSliders[i]) knobDragging[i] = false; } void SM26AudioProcessorEditor::updateLCD() { if (! menu.isOpen()) { int presetIdx = proc.engine.presetIndex(); const auto* p = sm26::PresetStore::findByIndex (presetIdx); lcd.setLine (0, juce::String (presetIdx + 1) + "/" + juce::String (sm26::PresetStore::count()) + " " + sm26::PresetStore::fullLabel (*p)); lcd.setLine (1, padRight (proc.engine.knobView (0).name, 12) + padRight (sm26::formatParamReal (proc.engine.knobView(0).param, proc.engine.getParam(proc.engine.knobView(0).param)), 12) + " " + padRight (proc.engine.knobView (2).name, 12) + sm26::formatParamReal (proc.engine.knobView(2).param, proc.engine.getParam(proc.engine.knobView(2).param))); lcd.setLine (2, padRight (proc.engine.knobView (1).name, 12) + padRight (sm26::formatParamReal (proc.engine.knobView(1).param, proc.engine.getParam(proc.engine.knobView(1).param)), 12) + " " + padRight (proc.engine.knobView (3).name, 12) + sm26::formatParamReal (proc.engine.knobView(3).param, proc.engine.getParam(proc.engine.knobView(3).param))); double bpm = proc.engine.lastBpm(); bool playing = proc.engine.transport(); juce::String bpmStr = playing ? juce::String (bpm, 1) + " BPM" : "--- BPM"; int notes = proc.engine.notesPlaying(); juce::String notesStr = notes > 0 ? "NOTES:" + juce::String (notes) : ""; lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr + " UP/DN=PATCH OK=MENU"); lcd.showCursor (false); } else { const auto& cs = menu.current(); lcd.setLine (0, "== " + cs.title + " =="); for (int row = 0; row < 3 && row < (int) cs.items.size(); ++row) { int idx = juce::jlimit (0, (int) cs.items.size() - 1, cs.selected + row); const auto& item = cs.items[idx]; juce::String line = (idx == cs.selected ? "> " : " ") + item.label; if (item.kind == MenuItem::Value && item.valueText) line = padRight (line, 50) + item.valueText(); else if (item.kind == MenuItem::Submenu) line = line + " [ENTER]"; lcd.setLine (1 + row, line); } lcd.setLine (3, "UP/DN=SEL L/R=VAL OK=OK ESC=BACK"); lcd.showCursor (false); } }