2026-09-11 16:38:28 +02:00
|
|
|
#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)
|
|
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
setSize (920, 680);
|
2026-09-11 16:38:28 +02:00
|
|
|
setResizable (true, false);
|
2026-09-13 21:19:49 +02:00
|
|
|
setResizeLimits (760, 620, 1500, 1300);
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
addAndMakeVisible (lcd);
|
|
|
|
|
setupKnobs();
|
|
|
|
|
setupNavButtons();
|
|
|
|
|
|
|
|
|
|
footer.setText (buildGitFooter(), juce::dontSendNotification);
|
|
|
|
|
footer.setJustificationType (juce::Justification::centredLeft);
|
2026-09-13 21:19:49 +02:00
|
|
|
footer.setColour (juce::Label::textColourId, juce::Colour (0xFFA0A0A0));
|
2026-09-11 16:38:28 +02:00
|
|
|
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);
|
2026-09-13 21:19:49 +02:00
|
|
|
knobSliders[i].setColour (juce::Slider::rotarySliderFillColourId, juce::Colour (0xFFC8C8C8));
|
|
|
|
|
knobSliders[i].setColour (juce::Slider::rotarySliderOutlineColourId, juce::Colour (0xFF2A2A2A));
|
2026-09-11 17:06:23 +02:00
|
|
|
knobSliders[i].setColour (juce::Slider::thumbColourId, juce::Colours::transparentBlack);
|
2026-09-11 16:38:28 +02:00
|
|
|
knobSliders[i].addListener (this);
|
|
|
|
|
addAndMakeVisible (knobSliders[i]);
|
|
|
|
|
|
|
|
|
|
knobLabels[i].setJustificationType (juce::Justification::centred);
|
2026-09-13 21:19:49 +02:00
|
|
|
knobLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFFA0A0A0));
|
|
|
|
|
knobLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 13.0f, juce::Font::plain }));
|
2026-09-11 16:38:28 +02:00
|
|
|
addAndMakeVisible (knobLabels[i]);
|
|
|
|
|
|
|
|
|
|
knobValueLabels[i].setJustificationType (juce::Justification::centred);
|
2026-09-13 21:19:49 +02:00
|
|
|
knobValueLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFFD0D0D0));
|
|
|
|
|
knobValueLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 14.0f, juce::Font::bold }));
|
2026-09-11 16:38:28 +02:00
|
|
|
addAndMakeVisible (knobValueLabels[i]);
|
|
|
|
|
|
|
|
|
|
knobSliders[i].setValue (0.5, juce::dontSendNotification);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 21:53:16 +02:00
|
|
|
juce::Font SM26AudioProcessorEditor::NavButtonLookAndFeel::getTextButtonFont (juce::TextButton&, int)
|
|
|
|
|
{
|
|
|
|
|
return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(),
|
|
|
|
|
10.5f, juce::Font::bold });
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 16:38:28 +02:00
|
|
|
void SM26AudioProcessorEditor::setupNavButtons()
|
|
|
|
|
{
|
|
|
|
|
auto setup = [this](juce::TextButton& btn, juce::Colour bg)
|
|
|
|
|
{
|
2026-09-12 21:53:16 +02:00
|
|
|
btn.setLookAndFeel (&navLookAndFeel);
|
2026-09-11 16:38:28 +02:00
|
|
|
btn.setColour (juce::TextButton::buttonColourId, bg);
|
2026-09-13 21:19:49 +02:00
|
|
|
btn.setColour (juce::TextButton::textColourOffId, juce::Colour (0xFFE0E0E0));
|
|
|
|
|
btn.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF444444));
|
2026-09-11 16:38:28 +02:00
|
|
|
addAndMakeVisible (btn);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
setup (navUp, juce::Colour (0xFF2A2A2A));
|
|
|
|
|
setup (navDown, juce::Colour (0xFF2A2A2A));
|
|
|
|
|
setup (navLeft, juce::Colour (0xFF2A2A2A));
|
|
|
|
|
setup (navRight, juce::Colour (0xFF2A2A2A));
|
|
|
|
|
setup (navEnter, juce::Colour (0xFF3F3F3F));
|
|
|
|
|
setup (navEsc, juce::Colour (0xFF333333));
|
2026-09-11 16:38:28 +02:00
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
navUp.setTriggeredOnMouseDown (true);
|
|
|
|
|
navUp.setRepeatSpeed (250, 50);
|
|
|
|
|
navDown.setTriggeredOnMouseDown (true);
|
|
|
|
|
navDown.setRepeatSpeed (250, 50);
|
2026-09-12 21:53:16 +02:00
|
|
|
navUp.onClick = [this] { if (menu.isOpen()) menu.up(); else cyclePreset (-1); };
|
|
|
|
|
navDown.onClick = [this] { if (menu.isOpen()) menu.down(); else cyclePreset (+1); };
|
2026-09-11 16:38:28 +02:00
|
|
|
navLeft.onClick = [this] { menu.left(); };
|
|
|
|
|
navRight.onClick = [this] { menu.right(); };
|
2026-09-13 21:19:49 +02:00
|
|
|
navEnter.onClick = [this] { if (menu.isOpen()) menu.enter(); else menu.open(); };
|
2026-09-11 16:38:28 +02:00
|
|
|
navEsc.onClick = [this] { if (menu.isOpen()) menu.escape(); else menu.close(); };
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 21:53:16 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 16:38:28 +02:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
auto addValue = [&](const char* n, int pid) -> MenuItem
|
|
|
|
|
{
|
|
|
|
|
MenuItem mi;
|
|
|
|
|
mi.label = n;
|
|
|
|
|
mi.kind = MenuItem::Value;
|
|
|
|
|
mi.paramId = pid;
|
|
|
|
|
mi.valueText = [this, pid]() {
|
|
|
|
|
if (pid == sm26::PDTIME && proc.engine.getParam (sm26::PDSYNC) > 0.5f)
|
|
|
|
|
return sm26::formatTimeMs ((float) proc.engine.delayTimeMs());
|
|
|
|
|
return sm26::formatParamReal (pid, proc.engine.getParam (pid));
|
|
|
|
|
};
|
|
|
|
|
mi.onAdjust = [this, pid](float d) {
|
|
|
|
|
float delta = (pid == sm26::PDSTEPS) ? d / 0.02f * (1.0f / 15.0f) : d;
|
|
|
|
|
float cur = proc.engine.getParam (pid);
|
|
|
|
|
proc.engine.setParam (pid, sm26::clamp01 (cur + delta));
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return mi;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto addHeader = [&](const char* n) {
|
|
|
|
|
MenuItem h;
|
|
|
|
|
h.label = "-- " + juce::String (n) + " --";
|
|
|
|
|
h.kind = MenuItem::Separator;
|
|
|
|
|
s.items.push_back (h);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-11 16:38:28 +02:00
|
|
|
{
|
|
|
|
|
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();
|
2026-09-13 21:19:49 +02:00
|
|
|
juce::String lastCat;
|
|
|
|
|
const int count = sm26::PresetStore::count();
|
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
|
{
|
|
|
|
|
const auto* p = sm26::PresetStore::get (i);
|
|
|
|
|
if (lastCat != p->category)
|
|
|
|
|
{
|
|
|
|
|
MenuItem h;
|
|
|
|
|
h.label = "---- " + juce::String (p->category) + " ----";
|
|
|
|
|
h.kind = MenuItem::Separator;
|
|
|
|
|
ps.items.push_back (h);
|
|
|
|
|
lastCat = p->category;
|
|
|
|
|
}
|
|
|
|
|
ps.items.push_back (addPreset (p->name, i));
|
|
|
|
|
}
|
2026-09-11 16:38:28 +02:00
|
|
|
menu.pushScreen (ps);
|
|
|
|
|
};
|
|
|
|
|
s.items.push_back (m);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
addHeader ("OSCILLATORS");
|
|
|
|
|
s.items.push_back (addValue ("OSC1 WAVE", sm26::PO1WAVE));
|
|
|
|
|
s.items.push_back (addValue ("OSC2 WAVE", sm26::PO2WAVE));
|
|
|
|
|
s.items.push_back (addValue ("O1 OCTAVE", sm26::PO1OCT));
|
|
|
|
|
s.items.push_back (addValue ("O1 DETUNE", sm26::PO1CT));
|
|
|
|
|
s.items.push_back (addValue ("O2 OCTAVE", sm26::PO2OCT));
|
|
|
|
|
s.items.push_back (addValue ("O2 DETUNE", sm26::PO2CT));
|
|
|
|
|
s.items.push_back (addValue ("MIX", sm26::PMIX));
|
|
|
|
|
s.items.push_back (addValue ("NOISE", sm26::PNOISE));
|
2026-09-11 16:38:28 +02:00
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
addHeader ("FILTER");
|
|
|
|
|
s.items.push_back (addValue ("CUTOFF", sm26::PCUTOFF));
|
|
|
|
|
s.items.push_back (addValue ("RES", sm26::PRES));
|
|
|
|
|
s.items.push_back (addValue ("F-ENV", sm26::PFENV));
|
|
|
|
|
s.items.push_back (addValue ("F-TYPE", sm26::PFTYPE));
|
|
|
|
|
|
|
|
|
|
addHeader ("ENVELOPE");
|
|
|
|
|
s.items.push_back (addValue ("ATTACK", sm26::PATK));
|
|
|
|
|
s.items.push_back (addValue ("DECAY", sm26::PDEC));
|
|
|
|
|
s.items.push_back (addValue ("SUSTAIN", sm26::PSUS));
|
|
|
|
|
s.items.push_back (addValue ("RELEASE", sm26::PREL));
|
|
|
|
|
|
|
|
|
|
addHeader ("DELAY");
|
|
|
|
|
s.items.push_back (addValue ("TIME/STEPS", sm26::PDTIME));
|
|
|
|
|
s.items.push_back (addValue ("STEPS", sm26::PDSTEPS));
|
|
|
|
|
s.items.push_back (addValue ("SYNC", sm26::PDSYNC));
|
|
|
|
|
s.items.push_back (addValue ("FEEDBACK", sm26::PDFB));
|
|
|
|
|
s.items.push_back (addValue ("MIX", sm26::PDMIX));
|
|
|
|
|
|
|
|
|
|
addHeader ("SYSTEM");
|
|
|
|
|
s.items.push_back (addValue ("DRIVE", sm26::PDRIVE));
|
|
|
|
|
s.items.push_back (addValue ("GLIDE", sm26::PGLIDE));
|
|
|
|
|
s.items.push_back (addValue ("MASTER", sm26::PMASTER));
|
|
|
|
|
|
|
|
|
|
addHeader ("INFO");
|
2026-09-11 16:38:28 +02:00
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
MenuItem mi;
|
|
|
|
|
mi.label = "ABOUT";
|
|
|
|
|
mi.kind = MenuItem::Action;
|
|
|
|
|
mi.onEnter = [this]() {
|
|
|
|
|
MenuScreen as;
|
|
|
|
|
as.title = "ABOUT";
|
|
|
|
|
as.selected = 0;
|
2026-09-11 16:38:28 +02:00
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
MenuItem m2; m2.label = "SM-26 Sound Module by A. Jenewein"; m2.kind = MenuItem::Action;
|
|
|
|
|
as.items.push_back (m2);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
|
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
MenuItem m2;
|
|
|
|
|
m2.label = "Version: " + juce::String (SM26_GIT_VERSION);
|
|
|
|
|
m2.kind = MenuItem::Action;
|
|
|
|
|
as.items.push_back (m2);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
2026-09-13 21:19:49 +02:00
|
|
|
{
|
|
|
|
|
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 = juce::String (sm26::PresetStore::count()) + " presets | VST3 + AU";
|
|
|
|
|
m2.kind = MenuItem::Action;
|
|
|
|
|
as.items.push_back (m2);
|
|
|
|
|
}
|
|
|
|
|
menu.pushScreen (as);
|
2026-09-11 16:38:28 +02:00
|
|
|
};
|
2026-09-13 21:19:49 +02:00
|
|
|
s.items.push_back (mi);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
menu.setRootScreenBuilder (builder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SM26AudioProcessorEditor::paint (juce::Graphics& g)
|
|
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
g.fillAll (juce::Colour (0xFF000000));
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
const float totalW = (float) getWidth();
|
|
|
|
|
const float totalH = (float) getHeight();
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
g.setColour (juce::Colour (0xFF1A1A1A));
|
2026-09-11 16:38:28 +02:00
|
|
|
g.fillRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f);
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
g.setColour (juce::Colour (0xFF333333));
|
2026-09-11 16:38:28 +02:00
|
|
|
g.drawRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f, 1.5f);
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
g.setColour (juce::Colour (0xFFC8C8C8));
|
|
|
|
|
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);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SM26AudioProcessorEditor::resized()
|
|
|
|
|
{
|
|
|
|
|
const float totalW = (float) getWidth();
|
|
|
|
|
const float totalH = (float) getHeight();
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
float knobSize = 78.0f;
|
|
|
|
|
float knobGap = 20.0f;
|
2026-09-11 16:38:28 +02:00
|
|
|
float knobsW = 4.0f * knobSize + 3.0f * knobGap;
|
|
|
|
|
float knobsX = (totalW - knobsW) / 2.0f;
|
2026-09-13 21:19:49 +02:00
|
|
|
float knobsY = juce::jmax (360.0f, totalH - 300.0f);
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
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);
|
2026-09-13 21:19:49 +02:00
|
|
|
knobLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 2), (int) knobSize, 18);
|
|
|
|
|
knobValueLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 20), (int) knobSize, 16);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-11 17:06:23 +02:00
|
|
|
float navSize = 30.0f;
|
|
|
|
|
float navGap = 3.0f;
|
2026-09-11 16:38:28 +02:00
|
|
|
float navCenterX = totalW / 2.0f;
|
2026-09-13 21:19:49 +02:00
|
|
|
float legendBottom = knobsY + knobSize + 20.0f + 16.0f;
|
|
|
|
|
float footerTop = totalH - 22.0f;
|
|
|
|
|
float navCenterY = (legendBottom + footerTop) * 0.5f;
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
lcd.setBounds (14, 16, (int) totalW - 28, 302);
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
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);
|
2026-09-12 21:53:16 +02:00
|
|
|
if (! knobDragging[i])
|
|
|
|
|
{
|
|
|
|
|
float pos = sm26::clamp01 ((val - kv.lo) / (kv.hi - kv.lo + 1e-9f));
|
|
|
|
|
knobSliders[i].setValue (pos, juce::dontSendNotification);
|
|
|
|
|
}
|
2026-09-11 16:38:28 +02:00
|
|
|
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);
|
2026-09-12 21:53:16 +02:00
|
|
|
knobValueLabels[i].setText (sm26::formatParamReal (kv.param, paramVal),
|
|
|
|
|
juce::dontSendNotification);
|
2026-09-11 16:38:28 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 21:53:16 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 16:38:28 +02:00
|
|
|
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));
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
for (int k = 0; k < 4; ++k)
|
|
|
|
|
{
|
|
|
|
|
auto kv = proc.engine.knobView (k);
|
|
|
|
|
lcd.setLine (1 + k, padRight (kv.name, 20)
|
|
|
|
|
+ sm26::formatParamReal (kv.param, proc.engine.getParam (kv.param)));
|
|
|
|
|
}
|
2026-09-11 16:38:28 +02:00
|
|
|
|
|
|
|
|
double bpm = proc.engine.lastBpm();
|
2026-09-13 21:19:49 +02:00
|
|
|
juce::String bpmStr = juce::String (bpm, 1) + " BPM";
|
2026-09-11 16:38:28 +02:00
|
|
|
int notes = proc.engine.notesPlaying();
|
|
|
|
|
juce::String notesStr = notes > 0 ? "NOTES:" + juce::String (notes) : "";
|
|
|
|
|
|
2026-09-13 21:19:49 +02:00
|
|
|
lcd.setLine (6, bpmStr + (notesStr.isNotEmpty() ? " " + notesStr : ""));
|
|
|
|
|
lcd.setLine (7, juce::String ("SM-26") + " UP/DN=PATCH OK=MENU");
|
|
|
|
|
for (int r = 0; r < LcdDisplay::kRows; ++r)
|
|
|
|
|
lcd.setInverted (r, false);
|
2026-09-11 16:38:28 +02:00
|
|
|
lcd.showCursor (false);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
const auto& cs = menu.current();
|
|
|
|
|
|
|
|
|
|
lcd.setLine (0, "== " + cs.title + " ==");
|
2026-09-13 21:19:49 +02:00
|
|
|
|
|
|
|
|
for (int r = 0; r < LcdDisplay::kRows; ++r)
|
|
|
|
|
lcd.setInverted (r, false);
|
|
|
|
|
|
|
|
|
|
std::vector<int> shown;
|
|
|
|
|
for (int i = 0; i < (int) cs.items.size(); ++i)
|
2026-09-11 16:38:28 +02:00
|
|
|
{
|
2026-09-13 21:19:49 +02:00
|
|
|
const auto& it = cs.items[i];
|
|
|
|
|
if (it.kind == MenuItem::Separator && it.label.isEmpty())
|
|
|
|
|
continue;
|
|
|
|
|
shown.push_back (i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int pos = 0;
|
|
|
|
|
for (int i = 0; i < (int) shown.size(); ++i)
|
|
|
|
|
if (shown[i] == cs.selected) { pos = i; break; }
|
|
|
|
|
|
|
|
|
|
int top = juce::jlimit (0, juce::jmax (0, (int) shown.size() - 6), pos - 2);
|
|
|
|
|
for (int row = 0; row < 6 && top + row < (int) shown.size(); ++row)
|
|
|
|
|
{
|
|
|
|
|
int idx = shown[top + row];
|
2026-09-11 16:38:28 +02:00
|
|
|
const auto& item = cs.items[idx];
|
2026-09-13 21:19:49 +02:00
|
|
|
juce::String line = " " + item.label;
|
2026-09-11 16:38:28 +02:00
|
|
|
if (item.kind == MenuItem::Value && item.valueText)
|
|
|
|
|
line = padRight (line, 50) + item.valueText();
|
|
|
|
|
else if (item.kind == MenuItem::Submenu)
|
|
|
|
|
line = line + " [ENTER]";
|
2026-09-13 21:19:49 +02:00
|
|
|
else if (item.kind == MenuItem::Separator)
|
|
|
|
|
line = " " + item.label;
|
2026-09-11 16:38:28 +02:00
|
|
|
lcd.setLine (1 + row, line);
|
2026-09-13 21:19:49 +02:00
|
|
|
lcd.setInverted (1 + row, idx == cs.selected);
|
2026-09-11 16:38:28 +02:00
|
|
|
}
|
2026-09-13 21:19:49 +02:00
|
|
|
lcd.setLine (7, "UP/DN=SEL L/R=VAL OK=OK ESC=BACK");
|
2026-09-11 16:38:28 +02:00
|
|
|
lcd.showCursor (false);
|
|
|
|
|
}
|
2026-09-13 21:19:49 +02:00
|
|
|
}
|