sm-26/Source/PluginEditor.cpp

588 lines
23 KiB
C++
Raw Normal View History

#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);
}
}
void SM26AudioProcessorEditor::setupNavButtons()
{
auto setup = [this](juce::TextButton& btn, juce::Colour bg)
{
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] { menu.up(); };
navDown.onClick = [this] { menu.down(); };
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::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 = "48 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 = totalH - 135.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);
knobSliders[i].setValue (val, 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);
break;
}
}
}
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);
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/DOWN=SEL L/R=VAL ENTER=OK ESC=BACK");
lcd.showCursor (false);
}
}