SM-26 synthesizer: initial implementation

This commit is contained in:
SM-26 2026-09-11 16:38:28 +02:00
commit 8734ac25f1
18 changed files with 2619 additions and 0 deletions

155
Source/LcdDisplay.cpp Normal file
View file

@ -0,0 +1,155 @@
#include "LcdDisplay.h"
LcdDisplay::LcdDisplay()
{
clear();
startTimerHz (30);
}
juce::Font LcdDisplay::getMonoFont() const
{
return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(),
(float) kCharH - 1.0f, juce::Font::plain });
}
void LcdDisplay::rasterize (juce::uint32 c)
{
if (c >= 256 || glyphCache[c].cached) return;
juce::Image img (juce::Image::ARGB, kCharW * 2, kCharH * 2, true);
{
juce::Graphics g (img);
g.setColour (juce::Colours::white);
g.setFont (getMonoFont());
juce::String s;
s << (juce_wchar) c;
g.drawText (s, img.getBounds().removeFromBottom (kCharH * 2 - 2),
juce::Justification::centredBottom, false);
}
const float sx = (float) img.getWidth() / (float) kCharW;
const float sy = (float) img.getHeight() / (float) kCharH;
for (int row = 0; row < 8; ++row)
glyphCache[c].rows[row] = 0;
for (int row = 0; row < 8; ++row)
{
uint8_t bits = 0;
for (int col = 0; col < 5; ++col)
{
int count = 0, total = 0;
int px0 = (int) (col * sx);
int py0 = (int) (row * sy);
int px1 = juce::jmin ((int) ((col + 1) * sx), img.getWidth() - 1);
int py1 = juce::jmin ((int) ((row + 1) * sy), img.getHeight() - 1);
for (int y = py0; y <= py1; ++y)
for (int x = px0; x <= px1; ++x)
{
total++;
if (img.getPixelAt (x, y).getAlpha() > 127)
count++;
}
if (total > 0 && count > total / 3)
bits |= (uint8_t) (1u << (4 - col));
}
glyphCache[c].rows[row] = bits;
}
glyphCache[c].cached = true;
}
uint8_t* LcdDisplay::getGlyph (juce::uint32 c)
{
if (c >= 256) c = '?';
if (! glyphCache[c].cached)
rasterize (c);
return glyphCache[c].rows;
}
void LcdDisplay::setLine (int row, const juce::String& s)
{
if (row >= 0 && row < kRows)
lines[row] = s;
}
void LcdDisplay::showCursor (bool show) { cursorVisible = show; }
void LcdDisplay::clear()
{
for (int r = 0; r < kRows; ++r)
lines[r] = "";
cursorCol = 0;
cursorRow = 0;
cursorVisible = false;
}
void LcdDisplay::paint (juce::Graphics& g)
{
const float cw = (float) kCharW;
const float ch = (float) kCharH;
const float w = (float) getWidth();
const float h = (float) getHeight();
const juce::Colour bgCol (0xFF0D1A05);
const juce::Colour gridCol (0xFF15260A);
const juce::Colour pixOn (0xFF4AE038);
const juce::Colour pixOnDim (0xFF1E3A12);
const juce::Colour pixOff (0xFF0F1C07);
g.fillAll (bgCol);
g.setColour (gridCol);
float totalW = (float) kCols * cw;
float totalH = (float) kRows * ch;
float ox = (w - totalW) / 2.0f;
float oy = (h - totalH) / 2.0f;
g.fillRect (ox - 1.0f, oy - 1.0f, totalW + 2.0f, totalH + 2.0f);
bool blinkOn = cursorVisible &&
((int) (juce::Time::getMillisecondCounterHiRes() / 400.0) % 2 == 0);
for (int row = 0; row < kRows; ++row)
{
const juce::String& line = lines[row];
for (int col = 0; col < kCols; ++col)
{
juce_wchar ch2 = col < line.length() ? line [(size_t) col] : (juce_wchar) ' ';
uint8_t* glyph = getGlyph ((juce::uint32) ch2);
float cx = ox + (float) col * cw;
float cy = oy + (float) row * ch;
g.setColour (pixOff);
g.fillRect (cx + 0.5f, cy + 0.5f, cw - 1.0f, ch - 1.0f);
for (int gRow = 0; gRow < 8; ++gRow)
{
uint8_t bits = glyph[gRow];
if (bits == 0) continue;
for (int gCol = 0; gCol < 5; ++gCol)
{
if (bits & (1u << (4 - gCol)))
{
float px = cx + 1.0f + (float) gCol * (cw - 2.0f) / 5.0f;
float py = cy + 1.0f + (float) gRow * (ch - 2.0f) / 9.0f;
float pw = (cw - 2.0f) / 5.0f - 0.5f;
float ph = (ch - 2.0f) / 9.0f - 0.3f;
g.setColour (pixOnDim);
g.fillRect (px - 0.4f, py - 0.3f, pw + 0.8f, ph + 0.6f);
g.setColour (pixOn);
g.fillRect (px, py, pw, ph);
}
}
}
if (blinkOn && col == cursorCol && row == cursorRow)
{
g.setColour (pixOn);
g.fillRect (cx + 1.0f, cy + ch - 3.0f, cw - 2.0f, 2.0f);
}
}
}
}
void LcdDisplay::timerCallback() { repaint(); }

42
Source/LcdDisplay.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <JuceHeader.h>
class LcdDisplay : public juce::Component,
private juce::Timer
{
public:
static constexpr int kCols = 80;
static constexpr int kRows = 4;
LcdDisplay();
void setLine (int row, const juce::String& s);
void showCursor (bool show);
void clear();
void paint (juce::Graphics&) override;
private:
void timerCallback() override;
static constexpr int kCharW = 6;
static constexpr int kCharH = 9;
juce::String lines[kRows];
int cursorCol = 0;
int cursorRow = 0;
bool cursorVisible = false;
struct GlyphCache {
uint8_t rows[8] = {};
bool cached = false;
};
GlyphCache glyphCache[256];
void rasterize (juce::uint32 c);
uint8_t* getGlyph (juce::uint32 c);
juce::Font getMonoFont() const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LcdDisplay)
};

124
Source/MenuSystem.cpp Normal file
View file

@ -0,0 +1,124 @@
#include "MenuSystem.h"
MenuSystem::MenuSystem() {}
void MenuSystem::setRootScreenBuilder (ScreenBuilder fn) { rootBuilder = fn; }
void MenuSystem::rebuildRoot()
{
if (rootBuilder)
{
MenuScreen root = rootBuilder();
stack.clear();
stack.push_back (root);
}
}
void MenuSystem::open()
{
wasOpen = true;
rebuildRoot();
}
void MenuSystem::close()
{
stack.clear();
wasOpen = false;
}
void MenuSystem::pushScreen (const MenuScreen& screen) { stack.push_back (screen); }
const MenuScreen& MenuSystem::current() const
{
if (stack.empty())
{
static MenuScreen empty;
empty.title = "";
empty.selected = 0;
return empty;
}
return stack.back();
}
void MenuSystem::refreshCurrent()
{
if (! stack.empty())
{
int sel = stack.back().selected;
stack.pop_back();
if (stack.empty() && rootBuilder)
{
MenuScreen root = rootBuilder();
root.selected = sel;
stack.push_back (root);
}
else if (! stack.empty())
{
stack.back().selected = sel;
}
}
}
void MenuSystem::up()
{
if (stack.empty()) return;
auto& s = stack.back();
if (s.items.empty()) return;
int newSel = s.selected - 1;
while (newSel >= 0 && s.items[(size_t) newSel].kind == MenuItem::Separator)
--newSel;
if (newSel >= 0)
s.selected = newSel;
}
void MenuSystem::down()
{
if (stack.empty()) return;
auto& s = stack.back();
if (s.items.empty()) return;
int newSel = s.selected + 1;
while (newSel < (int) s.items.size() && s.items[(size_t) newSel].kind == MenuItem::Separator)
++newSel;
if (newSel < (int) s.items.size())
s.selected = newSel;
}
void MenuSystem::left()
{
if (stack.empty()) return;
auto& s = stack.back();
if (s.items.empty()) return;
auto& item = s.items[(size_t) s.selected];
if (item.kind == MenuItem::Value && item.onAdjust)
item.onAdjust (-0.02f);
}
void MenuSystem::right()
{
if (stack.empty()) return;
auto& s = stack.back();
if (s.items.empty()) return;
auto& item = s.items[(size_t) s.selected];
if (item.kind == MenuItem::Value && item.onAdjust)
item.onAdjust (0.02f);
}
void MenuSystem::enter()
{
if (stack.empty()) return;
auto& s = stack.back();
if (s.items.empty()) return;
auto& item = s.items[(size_t) s.selected];
if (item.onEnter)
item.onEnter();
}
void MenuSystem::escape()
{
if (stack.size() > 1)
stack.pop_back();
else if (stack.size() == 1)
close();
}

66
Source/MenuSystem.h Normal file
View file

@ -0,0 +1,66 @@
#pragma once
#include <JuceHeader.h>
#include "SynthParams.h"
class MenuSystem;
struct MenuItem
{
enum Kind { Action, Value, Submenu, Separator };
juce::String label;
Kind kind = Action;
int paramId = -1;
std::function<void()> onEnter;
std::function<void(float delta)> onAdjust;
std::function<juce::String()> valueText;
MenuItem& action (std::function<void()> fn) { onEnter = fn; return *this; }
MenuItem& param (int id, float lo, float hi) { paramId = id; return *this; }
MenuItem& value (std::function<juce::String()> fn) { valueText = fn; kind = Value; return *this; }
MenuItem& submenu() { kind = Submenu; return *this; }
MenuItem& sep() { kind = Separator; return *this; }
};
struct MenuScreen
{
juce::String title;
std::vector<MenuItem> items;
int selected = 0;
};
class MenuSystem
{
public:
MenuSystem();
void open();
void close();
bool isOpen() const { return ! stack.empty(); }
void up();
void down();
void left();
void right();
void enter();
void escape();
const MenuScreen& current() const;
void pushScreen (const MenuScreen& screen);
using ScreenBuilder = std::function<MenuScreen()>;
void setRootScreenBuilder (ScreenBuilder fn);
void refreshCurrent();
private:
std::vector<MenuScreen> stack;
ScreenBuilder rootBuilder;
bool wasOpen = false;
void rebuildRoot();
};

588
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,588 @@
#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::Colour (0xFFE0E0C0));
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 = 36.0f;
float navGap = 4.0f;
float navCenterX = totalW / 2.0f;
float navCenterY = totalH - 48.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, 36, (int) totalW - 28, (int) totalH - 190);
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);
}
}

53
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,53 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "LcdDisplay.h"
#include "MenuSystem.h"
#include "Presets.h"
#include "SynthParams.h"
class SM26AudioProcessorEditor : public juce::AudioProcessorEditor,
private juce::Timer,
private juce::Slider::Listener
{
public:
SM26AudioProcessorEditor (SM26AudioProcessor&);
~SM26AudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
void timerCallback() override;
private:
void sliderValueChanged (juce::Slider*) override;
void setupKnobs();
void setupNavButtons();
void buildRootMenu();
void updateLCD();
SM26AudioProcessor& proc;
MenuSystem menu;
LcdDisplay lcd;
juce::Slider knobSliders[4];
juce::Label knobLabels[4];
juce::Label knobValueLabels[4];
juce::TextButton navUp { "UP" };
juce::TextButton navDown { "DOWN" };
juce::TextButton navLeft { "LEFT" };
juce::TextButton navRight { "RIGHT" };
juce::TextButton navEnter { "ENTER" };
juce::TextButton navEsc { "ESC" };
juce::Label footer;
bool menuWasOpen = false;
static juce::String buildGitFooter();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SM26AudioProcessorEditor)
};

163
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,163 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "BuildInfo_generated.h"
SM26AudioProcessor::SM26AudioProcessor()
: AudioProcessor (BusesProperties().withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
apvts (*this, nullptr, "SM26State", {
std::make_unique<juce::AudioParameterFloat> ("k1", "Knob 1", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
std::make_unique<juce::AudioParameterFloat> ("k2", "Knob 2", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
std::make_unique<juce::AudioParameterFloat> ("k3", "Knob 3", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
std::make_unique<juce::AudioParameterFloat> ("k4", "Knob 4", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
std::make_unique<juce::AudioParameterInt> ("preset", "Preset",
0, sm26::PresetStore::count() - 1, 0)
})
{
engine.setPresetIndex (0);
apvts.addParameterListener ("k1", this);
apvts.addParameterListener ("k2", this);
apvts.addParameterListener ("k3", this);
apvts.addParameterListener ("k4", this);
apvts.addParameterListener ("preset", this);
paramsReady.store (true);
}
SM26AudioProcessor::~SM26AudioProcessor()
{
apvts.removeParameterListener ("k1", this);
apvts.removeParameterListener ("k2", this);
apvts.removeParameterListener ("k3", this);
apvts.removeParameterListener ("k4", this);
apvts.removeParameterListener ("preset", this);
}
void SM26AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
engine.prepare (sampleRate, samplesPerBlock);
}
void SM26AudioProcessor::releaseResources() {}
void SM26AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
{
juce::ScopedNoDenormals noDenormals;
const int n = buffer.getNumSamples();
double bpm = 120.0;
bool playing = false;
if (auto* ph = getPlayHead())
{
auto pos = ph->getPosition();
if (pos.hasValue())
{
if (pos->getBpm().hasValue())
bpm = *pos->getBpm();
playing = pos->getIsPlaying();
}
}
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
buffer.clear (ch, 0, n);
engine.process (buffer, midi, n, bpm, playing);
}
juce::AudioProcessorEditor* SM26AudioProcessor::createEditor()
{
return new SM26AudioProcessorEditor (*this);
}
void SM26AudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto xmlStr = apvts.copyState().createXml()->toString();
destData.replaceAll (xmlStr.toRawUTF8(), (size_t) xmlStr.length());
}
void SM26AudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
auto xml = juce::XmlDocument::parse (juce::String::fromUTF8 ((const char*) data, sizeInBytes));
if (xml != nullptr)
{
auto tree = juce::ValueTree::fromXml (*xml);
if (tree.isValid())
{
paramsReady.store (false);
apvts.replaceState (tree);
paramsReady.store (true);
int idx = (int) *apvts.getRawParameterValue ("preset");
engine.setPresetIndex (idx);
applyPresetKnobs();
}
}
}
void SM26AudioProcessor::setKnob (int knob, float value01)
{
if (knob < 0 || knob >= 4) return;
auto* p = apvts.getParameter (juce::String ("k") + juce::String (knob + 1));
if (p != nullptr)
p->setValueNotifyingHost (p->convertTo0to1 (value01));
}
void SM26AudioProcessor::adjustParam (int paramId, float delta01)
{
float cur = engine.getParam (paramId);
engine.setParam (paramId, sm26::clamp01 (cur + delta01));
for (int k = 0; k < 4; ++k)
{
auto kv = engine.knobView (k);
if (kv.param == paramId)
{
float pval = engine.getParam (paramId);
float knobVal = sm26::clamp01 ((pval - kv.lo) / (kv.hi - kv.lo + 1e-9f));
setKnob (k, knobVal);
}
}
}
void SM26AudioProcessor::setPresetByIndex (int i)
{
auto* p = apvts.getParameter ("preset");
if (p != nullptr)
p->setValueNotifyingHost (p->convertTo0to1 ((float) i));
}
void SM26AudioProcessor::parameterChanged (const juce::String& parameterID, float newValue)
{
if (! paramsReady.load()) return;
if (parameterID == "preset")
{
int idx = (int) std::round (newValue * (float) (sm26::PresetStore::count() - 1));
idx = juce::jlimit (0, sm26::PresetStore::count() - 1, idx);
engine.setPresetIndex (idx);
applyPresetKnobs();
}
else if (parameterID == "k1" || parameterID == "k2" || parameterID == "k3" || parameterID == "k4")
{
int k = parameterID.getIntValue() - 1;
auto kv = engine.knobView (k);
float paramVal = sm26::clamp01 (kv.lo + newValue * (kv.hi - kv.lo));
engine.setParam (kv.param, paramVal);
}
}
void SM26AudioProcessor::applyPresetKnobs()
{
for (int k = 0; k < 4; ++k)
{
auto kv = engine.knobView (k);
float paramVal = engine.getParam (kv.param);
float knobVal = sm26::clamp01 ((paramVal - kv.lo) / (kv.hi - kv.lo + 1e-9f));
setKnob (k, knobVal);
}
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new SM26AudioProcessor();
}

52
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,52 @@
#pragma once
#include <JuceHeader.h>
#include "SynthEngine.h"
#include "Presets.h"
class SM26AudioProcessor : public juce::AudioProcessor,
public juce::AudioProcessorValueTreeState::Listener
{
public:
SM26AudioProcessor();
~SM26AudioProcessor() override;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "SM-26"; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram (int) override {}
const juce::String getProgramName (int) override { return {}; }
void changeProgramName (int, const juce::String&) override {}
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
void setKnob (int knob, float value01);
void adjustParam (int paramId, float delta01);
juce::AudioProcessorValueTreeState apvts;
sm26::SynthEngine engine;
void setPresetByIndex (int i);
int presetCount() const { return sm26::PresetStore::count(); }
private:
void parameterChanged (const juce::String& parameterID, float newValue) override;
void applyPresetKnobs();
std::atomic<bool> paramsReady{false};
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SM26AudioProcessor)
};

400
Source/Presets.cpp Normal file
View file

@ -0,0 +1,400 @@
#include "Presets.h"
namespace sm26 {
namespace {
struct Catalog {
Preset data[48];
int n = 0;
};
Catalog& catalog()
{
static Catalog cat;
if (cat.n == 0)
{
using P = std::pair<int, float>;
using C = ControlDef;
auto K = [](const char* n, int p, float lo, float hi) {
C c; c.name = n; c.param = p; c.lo = lo; c.hi = hi; return c;
};
auto add = [&](const char* name, const char* category,
const std::initializer_list<P>& delta,
const ControlDef (&ctl)[4])
{
Preset& p = cat.data[cat.n++];
p.name = name;
p.category = category;
p.base = defaultParams();
for (auto& d : delta)
p.base[d.first] = d.second;
for (int i = 0; i < 4; ++i)
p.ctl[i] = ctl[i];
};
const C cCff[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("F-ENV",PFENV,0.0f,1.0f), K("DELAY",PDMIX,0.15f,1.0f) };
const C cResDcy[4]= { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("DECAY",PDEC,0.15f,0.98f),K("DELAY",PDMIX,0.15f,1.0f) };
const C cEnvDcy[4]= { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.15f,0.98f),K("GLIDE",PGLIDE,0.0f,1.0f) };
const C cPluck[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("DECAY",PDEC,0.1f,0.9f), K("RES",PRES,0.0f,1.0f), K("DELAY",PDMIX,0.0f,0.8f) };
const C cPads[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("ATK",PATK,0.0f,1.0f), K("DECAY",PDEC,0.2f,0.95f), K("D-MIX",PDMIX,0.0f,1.0f) };
const C cDrift[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("DETUNE",PO1CT,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) };
const C cNoise[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("NOISE",PNOISE,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f), K("DELAY",PDMIX,0.0f,1.0f) };
const C cShp[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("F-TYPE",PFTYPE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) };
const C cKeys[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("DECAY",PDEC,0.1f,0.9f), K("DRIVE",PDRIVE,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) };
const C cStab[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("RES",PRES,0.0f,1.0f), K("F-ENV",PFENV,0.0f,1.0f), K("DRIVE",PDRIVE,0.0f,1.0f) };
// BASS ------------------------------------------------------------
add("Sub Square", "BASS",
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PMIX,0.9f},
{PCUTOFF,0.4f},{PRES,0.35f},{PATK,0.015f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.25f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.4f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.15f,0.8f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.15f,0.95f),K("D-MIX",PDMIX,0.0f,1.0f) });
add("Deep Saw", "BASS",
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.317f},{PO2OCT,0.36f},{PMIX,0.5f},
{PCUTOFF,0.42f},{PRES,0.25f},{PFENV,0.3f},{PATK,0.03f},{PDEC,0.45f},{PSUS,0.65f},{PREL,0.2f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.45f},{PDMIX,0.35f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.1f,0.7f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Acid Bass", "BASS",
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1OCT,0.283f},{PMIX,0.98f},{PCUTOFF,0.2f},{PRES,0.55f},
{PFENV,0.75f},{PATK,0.008f},{PDEC,0.6f},{PSUS,0.3f},{PREL,0.4f},
{PDSYNC,0.0f},{PDTIME,0.2f},{PDFB,0.6f},{PDMIX,0.3f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.04f,0.5f),K("RES",PRES,0.2f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
add("808 Thump", "BASS",
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.25f},{PO2OCT,0.24f},{PMIX,0.85f},
{PCUTOFF,0.3f},{PRES,0.7f},{PFENV,0.5f},{PATK,0.004f},{PDEC,0.65f},{PSUS,0.15f},{PREL,0.1f},
{PDSYNC,0.0f},{PMASTER,0.85f} },
{ K("CUTOFF",PCUTOFF,0.04f,0.6f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DRIVE",PDRIVE,0.0f,1.0f) });
add("DnB Reeses", "BASS",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.525f},{PO2CT,0.475f},{PMIX,0.5f},
{PCUTOFF,0.28f},{PRES,0.5f},{PFENV,0.6f},{PATK,0.01f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.15f},
{PDRIVE,0.5f},{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDFB,0.4f},{PDMIX,0.25f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.7f),K("RES",PRES,0.0f,1.0f),K("DETUNE",PO1CT,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f) });
add("Round Sub", "BASS",
{ {PO1WAVE,0.0f},{PO2WAVE,0.25f},{PO1OCT,0.25f},{PO2OCT,0.25f},{PMIX,0.5f},
{PCUTOFF,0.5f},{PRES,0.2f},{PATK,0.02f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.2f},
{PDSYNC,0.0f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,1.0f) });
add("Filter Zam", "BASS",
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.28f},{PO2OCT,0.35f},{PMIX,0.6f},
{PCUTOFF,0.18f},{PRES,0.7f},{PFENV,0.8f},{PATK,0.01f},{PDEC,0.35f},{PSUS,0.4f},{PREL,0.3f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.55f},{PDMIX,0.3f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.04f,0.5f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f) });
add("Dirty Tron", "BASS",
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PMIX,0.5f},
{PCUTOFF,0.35f},{PRES,0.4f},{PFENV,0.4f},{PATK,0.02f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.15f},
{PDRIVE,0.6f},{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.35f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.85f),K("RES",PRES,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
// LEAD ------------------------------------------------------------
add("Plain Sine", "LEAD",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PMIX,1.0f},{PCUTOFF,0.7f},{PRES,0.15f},{PFENV,0.3f},
{PATK,0.02f},{PDEC,0.3f},{PSUS,0.65f},{PREL,0.2f},{PGLIDE,0.2f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
cCff);
add("Square Lead", "LEAD",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.505f},{PMIX,0.5f},{PCUTOFF,0.55f},{PRES,0.3f},
{PFENV,0.35f},{PATK,0.01f},{PDEC,0.3f},{PSUS,0.7f},{PREL,0.15f},{PGLIDE,0.25f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.3f},{PMASTER,0.75f} },
cStab);
add("Saw Lead", "LEAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.5f},{PCUTOFF,0.5f},{PRES,0.25f},
{PFENV,0.4f},{PATK,0.01f},{PDEC,0.35f},{PSUS,0.7f},{PREL,0.12f},{PGLIDE,0.3f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.35f},{PMASTER,0.75f} },
cCff);
add("Distant Lead", "LEAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.25f},{PO1CT,0.53f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.3f},{PRES,0.4f},
{PFENV,0.55f},{PATK,0.005f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.35f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.8f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Glass Lead", "LEAD",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.62f},{PO2OCT,0.56f},{PO1CT,0.51f},{PO2CT,0.49f},
{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.45f},{PFENV,0.5f},{PATK,0.01f},{PDEC,0.4f},{PSUS,0.6f},{PREL,0.25f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.72f} },
cCff);
add("Supersaw Growl", "LEAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.45f},{PO2CT,0.55f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.3f},
{PFENV,0.45f},{PATK,0.02f},{PDEC,0.35f},{PSUS,0.7f},{PREL,0.15f},{PDRIVE,0.35f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
cDrift);
add("Night Rider", "LEAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1OCT,0.4f},{PO2OCT,0.5f},{PMIX,0.6f},{PCUTOFF,0.32f},{PRES,0.5f},
{PFENV,0.6f},{PATK,0.015f},{PDEC,0.3f},{PSUS,0.7f},{PREL,0.2f},{PDRIVE,0.4f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.4f},{PMASTER,0.78f} },
cShp);
add("Sine FM Bell", "LEAD",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.45f},{PCUTOFF,0.3f},{PRES,0.35f},{PFENV,0.7f},
{PATK,0.002f},{PDEC,0.4f},{PSUS,0.1f},{PREL,0.4f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.75f} },
{ K("RATIO",PO2OCT,0.0f,1.0f),K("LEVEL",PMIX,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
// PAD -------------------------------------------------------------
add("Analog Pad", "PAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.49f},{PO2CT,0.51f},{PMIX,0.45f},{PCUTOFF,0.35f},{PRES,0.3f},
{PFENV,0.45f},{PATK,0.45f},{PDEC,0.5f},{PSUS,0.7f},{PREL,0.6f},
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDFB,0.45f},{PDMIX,0.4f},{PMASTER,0.7f} },
cPads);
add("Dream Pad", "PAD",
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.52f},{PO2CT,0.48f},
{PMIX,0.5f},{PCUTOFF,0.38f},{PRES,0.42f},{PFENV,0.5f},{PATK,0.5f},{PDEC,0.45f},{PSUS,0.7f},{PREL,0.7f},
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.55f},{PDMIX,0.5f},{PMASTER,0.7f} },
cPads);
add("Dark Pad", "PAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.28f},{PO2OCT,0.35f},{PMIX,0.5f},{PCUTOFF,0.22f},{PRES,0.5f},
{PFENV,0.35f},{PATK,0.6f},{PDEC,0.45f},{PSUS,0.75f},{PREL,0.8f},
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.5f},{PDMIX,0.45f},{PMASTER,0.72f} },
cPads);
add("Brass Pad", "PAD",
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.4f},{PO2OCT,0.5f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
{PFENV,0.6f},{PATK,0.15f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.5f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.2f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("RES",PRES,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) });
add("String Swell", "PAD",
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1CT,0.53f},{PO2CT,0.47f},{PMIX,0.45f},{PCUTOFF,0.3f},{PRES,0.35f},
{PFENV,0.55f},{PATK,0.8f},{PDEC,0.35f},{PSUS,0.75f},{PREL,0.7f},
{PDSYNC,0.0f},{PMASTER,0.72f} },
cPads);
add("Shimmer Pad", "PAD",
{ {PO1WAVE,0.25f},{PO2WAVE,0.5f},{PO1OCT,0.62f},{PO2OCT,0.5f},{PO1CT,0.53f},{PO2CT,0.47f},
{PMIX,0.45f},{PCUTOFF,0.35f},{PRES,0.4f},{PFENV,0.5f},{PATK,0.4f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.8f},
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.65f},{PDMIX,0.6f},{PMASTER,0.7f} },
cPads);
add("Airy Pad", "PAD",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.47f},{PO2CT,0.52f},{PMIX,0.5f},{PNOISE,0.1f},{PCUTOFF,0.4f},
{PRES,0.25f},{PFENV,0.4f},{PATK,0.35f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.6f},
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDMIX,0.4f},{PMASTER,0.68f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("NOISE",PNOISE,0.0f,1.0f),K("DECAY",PDEC,0.2f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Vapor Pad", "PAD",
{ {PO1WAVE,0.75f},{PO2WAVE,0.25f},{PO1CT,0.55f},{PO2CT,0.48f},{PMIX,0.5f},{PCUTOFF,0.28f},{PRES,0.55f},
{PFENV,0.6f},{PATK,0.5f},{PDEC,0.5f},{PSUS,0.55f},{PREL,0.85f},
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.7f} },
cPads);
// KEYS ------------------------------------------------------------
add("DX Bell", "KEYS",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.4f},{PCUTOFF,0.5f},{PRES,0.3f},{PFENV,0.3f},
{PATK,0.002f},{PDEC,0.35f},{PSUS,0.1f},{PREL,0.5f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
cKeys);
add("Wurly", "KEYS",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.62f},{PMIX,0.5f},{PCUTOFF,0.32f},{PRES,0.45f},
{PFENV,0.5f},{PATK,0.004f},{PDEC,0.5f},{PSUS,0.2f},{PREL,0.3f},{PDRIVE,0.5f},
{PDSYNC,0.0f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
add("Rhodes EP", "KEYS",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.5f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.45f},
{PCUTOFF,0.42f},{PRES,0.35f},{PFENV,0.35f},{PATK,0.003f},{PDEC,0.45f},{PSUS,0.15f},{PREL,0.35f},
{PDRIVE,0.3f},{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.85f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
add("Clay EP", "KEYS",
{ {PO1WAVE,0.25f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.62f},{PMIX,0.6f},{PCUTOFF,0.38f},{PRES,0.4f},
{PFENV,0.45f},{PATK,0.006f},{PDEC,0.4f},{PSUS,0.15f},{PREL,0.3f},{PDRIVE,0.35f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.25f},{PMASTER,0.72f} },
cKeys);
add("Harpsi", "KEYS",
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
{PFENV,0.4f},{PATK,0.001f},{PDEC,0.5f},{PSUS,0.1f},{PREL,0.2f},{PDRIVE,0.3f},
{PDSYNC,0.0f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
add("Toy Piano", "KEYS",
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PCUTOFF,0.3f},{PRES,0.5f},
{PFENV,0.5f},{PATK,0.001f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.15f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.25f},{PMASTER,0.72f} },
cKeys);
add("Warm EP", "KEYS",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO1CT,0.52f},{PO2CT,0.48f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.3f},
{PFENV,0.4f},{PATK,0.004f},{PDEC,0.45f},{PSUS,0.3f},{PREL,0.3f},{PDRIVE,0.2f},
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDMIX,0.35f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("DELAY",PDMIX,0.0f,1.0f) });
// PLUCK -----------------------------------------------------------
add("Wood Pluck", "PLUCK",
{ {PO1WAVE,0.25f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.8f},{PMIX,0.6f},{PCUTOFF,0.32f},{PRES,0.55f},
{PFENV,0.7f},{PATK,0.001f},{PDEC,0.25f},{PSUS,0.0f},{PREL,0.2f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
cPluck);
add("Perc Pluck", "PLUCK",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.5f},{PO2CT,0.58f},{PMIX,0.5f},
{PCUTOFF,0.25f},{PRES,0.6f},{PFENV,0.6f},{PATK,0.001f},{PDEC,0.2f},{PSUS,0.0f},{PREL,0.2f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.78f} },
cPluck);
add("Guitar Pluck", "PLUCK",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.5f},{PO2CT,0.54f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.45f},
{PFENV,0.6f},{PATK,0.002f},{PDEC,0.35f},{PSUS,0.0f},{PREL,0.25f},
{PDSYNC,0.0f},{PMASTER,0.72f} },
cPluck);
add("Bubble", "PLUCK",
{ {PO1WAVE,0.0f},{PO2WAVE,0.25f},{PO2OCT,0.83f},{PMIX,0.4f},{PCUTOFF,0.3f},{PRES,0.6f},{PFENV,0.7f},
{PATK,0.003f},{PDEC,0.45f},{PSUS,0.0f},{PREL,0.4f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.5f},{PDMIX,0.4f},{PMASTER,0.75f} },
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("DECAY",PDEC,0.1f,0.95f),K("RES",PRES,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Kalimba", "PLUCK",
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.35f},{PCUTOFF,0.5f},{PRES,0.35f},{PFENV,0.4f},
{PATK,0.001f},{PDEC,0.4f},{PSUS,0.0f},{PREL,0.35f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
{ K("RATIO",PO2OCT,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("RES",PRES,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Karpling", "PLUCK",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.49f},{PO2CT,0.51f},{PMIX,0.5f},
{PCUTOFF,0.35f},{PRES,0.5f},{PFENV,0.5f},{PATK,0.001f},{PDEC,0.3f},{PSUS,0.0f},{PREL,0.2f},
{PDRIVE,0.2f},{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
cPluck);
// FX --------------------------------------------------------------
add("Wobble FX", "FX",
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1CT,0.58f},{PMIX,0.5f},{PCUTOFF,0.12f},{PRES,0.7f},{PFENV,0.9f},
{PATK,0.01f},{PDEC,0.5f},{PSUS,0.3f},{PREL,0.4f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.7f},{PDMIX,0.5f},{PMASTER,0.8f} },
cShp);
add("Riser", "FX",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.3f},{PO2OCT,0.4f},{PMIX,0.5f},{PNOISE,0.5f},{PCUTOFF,0.2f},
{PRES,0.5f},{PFENV,0.7f},{PATK,0.9f},{PDEC,0.1f},{PSUS,0.8f},{PREL,0.2f},
{PDSYNC,0.0f},{PMASTER,0.7f} },
{ K("RISE",PATK,0.2f,1.0f),K("CUTOFF",PCUTOFF,0.05f,1.0f),K("NOISE",PNOISE,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) });
add("Laser", "FX",
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PMIX,1.0f},{PCUTOFF,0.08f},{PRES,0.8f},{PFENV,0.8f},
{PATK,0.002f},{PDEC,0.4f},{PSUS,0.0f},{PREL,0.3f},
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.4f},{PDMIX,0.4f},{PMASTER,0.8f} },
cShp);
add("Sweep", "FX",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PNOISE,0.2f},{PCUTOFF,0.35f},
{PRES,0.45f},{PFENV,0.6f},{PATK,0.2f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.6f},
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.72f} },
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Drone FX", "FX",
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PO1CT,0.54f},{PO2CT,0.55f},{PMIX,0.5f},
{PNOISE,0.12f},{PCUTOFF,0.25f},{PRES,0.6f},{PFENV,0.2f},{PATK,0.8f},{PDEC,0.3f},{PSUS,0.8f},{PREL,0.8f},
{PDSYNC,1.0f},{PDSTEPS,16.0f},{PDFB,0.7f},{PDMIX,0.6f},{PMASTER,0.72f} },
cNoise);
add("Tuned Noise", "FX",
{ {PO1WAVE,0.8f},{PO2WAVE,0.8f},{PMIX,0.5f},{PNOISE,0.6f},{PCUTOFF,0.32f},{PRES,0.5f},{PFENV,0.5f},
{PATK,0.05f},{PDEC,0.4f},{PSUS,0.4f},{PREL,0.5f},
{PDSYNC,0.0f},{PMASTER,0.68f} },
cNoise);
// STAB / ARP ------------------------------------------------------
add("Detroit Stab", "STAB",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.52f},{PO2CT,0.48f},{PMIX,0.5f},
{PCUTOFF,0.4f},{PRES,0.45f},{PFENV,0.6f},{PATK,0.004f},{PDEC,0.35f},{PSUS,0.3f},{PREL,0.25f},{PDRIVE,0.3f},
{PDSYNC,0.0f},{PMASTER,0.8f} },
cStab);
add("Juno Stab", "STAB",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.48f},{PRES,0.35f},
{PFENV,0.5f},{PATK,0.003f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.2f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.78f} },
cStab);
add("Chord Stab", "STAB",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.35f},{PRES,0.5f},
{PFENV,0.65f},{PATK,0.004f},{PDEC,0.3f},{PSUS,0.4f},{PREL,0.3f},{PDRIVE,0.4f},
{PDSYNC,0.0f},{PMASTER,0.78f} },
cStab);
add("House Stab", "STAB",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.48f},{PO2CT,0.52f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.3f},
{PFENV,0.5f},{PATK,0.003f},{PDEC,0.3f},{PSUS,0.3f},{PREL,0.18f},
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.8f} },
cStab);
add("Arp Energy", "ARP",
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.52f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
{PFENV,0.5f},{PATK,0.008f},{PDEC,0.3f},{PSUS,0.5f},{PREL,0.15f},
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.5f},{PDMIX,0.45f},{PMASTER,0.78f} },
cCff);
add("Trance Gate", "ARP",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.3f},
{PFENV,0.55f},{PATK,0.01f},{PDEC,0.5f},{PSUS,0.4f},{PREL,0.12f},{PDRIVE,0.25f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.78f} },
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("DELAY",PDMIX,0.0f,1.0f) });
add("Neon Arp", "ARP",
{ {PO1WAVE,0.5f},{PO2WAVE,0.25f},{PO1OCT,0.62f},{PO2OCT,0.5f},{PO1CT,0.55f},{PO2CT,0.5f},{PMIX,0.5f},
{PCUTOFF,0.35f},{PRES,0.55f},{PFENV,0.7f},{PATK,0.003f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.3f},
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.78f} },
cCff);
add("Retro Arp", "ARP",
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.42f},{PRES,0.35f},
{PFENV,0.5f},{PATK,0.004f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.1f},
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.55f},{PDMIX,0.4f},{PMASTER,0.78f} },
cCff);
add("Dark Arp", "ARP",
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1CT,0.5f},{PO2CT,0.53f},{PMIX,0.5f},{PCUTOFF,0.25f},{PRES,0.6f},
{PFENV,0.8f},{PATK,0.002f},{PDEC,0.3f},{PSUS,0.1f},{PREL,0.3f},
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.65f},{PDMIX,0.5f},{PMASTER,0.75f} },
cShp);
}
return cat;
}
} // namespace
const Preset* PresetStore::all() { return catalog().data; }
int PresetStore::count() { return catalog().n; }
int PresetStore::indexOf(const char* name)
{
const auto& cat = catalog();
for (int i = 0; i < cat.n; ++i)
if (juce::String (cat.data[i].name) == name)
return i;
return 0;
}
const Preset* PresetStore::get(int index)
{
const auto& cat = catalog();
return cat.data + juce::jlimit (0, cat.n - 1, index);
}
String PresetStore::fullLabel(const Preset& p)
{
return String ("[") + p.category + "] " + p.name;
}
const Preset* PresetStore::findByIndex(int clampedIndex) { return get (clampedIndex); }
} // namespace sm26

23
Source/Presets.h Normal file
View file

@ -0,0 +1,23 @@
#pragma once
#include "SynthParams.h"
namespace sm26 {
class PresetStore
{
public:
static const Preset* all();
static int count();
static const Preset* get(int index);
static int indexOf(const char* name);
static const Preset* findByIndex(int clampedIndex);
static String fullLabel(const Preset& p);
private:
static Preset* table();
};
} // namespace sm26

343
Source/SynthEngine.cpp Normal file
View file

@ -0,0 +1,343 @@
#include "SynthEngine.h"
#include <cmath>
namespace sm26 {
SynthEngine::SynthEngine()
{
auto def = defaultParams();
for (int i = 0; i < kNumParams; ++i)
params[i] = def[i];
knobParams[0] = PCUTOFF;
knobParams[1] = PRES;
knobParams[2] = PFENV;
knobParams[3] = PDMIX;
knobNames[0] = "CUTOFF";
knobNames[1] = "RES";
knobNames[2] = "F-ENV";
knobNames[3] = "DELAY";
for (int i = 0; i < kNumKnobs; ++i)
{ knobLows[i] = 0.0f; knobHighs[i] = 1.0f; }
std::srand (12345);
}
void SynthEngine::prepare (double sr, int mb)
{
sampleRate = sr;
maxBlock = mb;
delay.prepare (sr);
allNotesOff();
reset();
}
void SynthEngine::reset()
{
allNotesOff();
delay.clear();
}
void SynthEngine::loadPreset (const Preset& p)
{
for (int i = 0; i < kNumParams; ++i)
params[i] = p.base[i];
for (int i = 0; i < kNumKnobs; ++i)
{
knobParams[i] = p.ctl[i].param;
knobNames[i] = p.ctl[i].name;
knobLows[i] = p.ctl[i].lo;
knobHighs[i] = p.ctl[i].hi;
}
allNotesOff();
}
void SynthEngine::setPresetIndex (int i)
{
currentPresetIndex = i;
loadPreset (*PresetStore::findByIndex (i));
}
void SynthEngine::setParam (int id, float n01)
{
if (id < 0 || id >= kNumParams) return;
params[id] = juce::jlimit (0.0f, 1.0f, n01);
}
float SynthEngine::getParam (int id) const
{
if (id < 0 || id >= kNumParams) return 0.0f;
return params[id];
}
SynthEngine::KnobView SynthEngine::knobView (int knob) const
{
if (knob < 0 || knob >= kNumKnobs) return {};
KnobView k;
k.name = knobNames[knob];
k.param = knobParams[knob];
k.value01 = getParam (k.param);
return k;
}
float SynthEngine::knobValue (int knob) const
{
if (knob < 0 || knob >= kNumKnobs) return 0.0f;
return getParam (knobParams[knob]);
}
const char* SynthEngine::waveNameFor (int index) const
{
return sm26::waveName (juce::jlimit (0, 4, index));
}
void SynthEngine::noteOn (int note, float vel)
{
Voice* v = findFreeOrSteal();
resetVoice (*v, note, vel);
}
void SynthEngine::noteOff (int note)
{
for (auto& v : voices)
if (v.active && v.note == note)
v.envState = 3;
}
void SynthEngine::allNotesOff()
{
for (auto& v : voices) { v.active = false; v.env = 0.0; v.envState = 0; }
activeNotes = 0;
}
SynthEngine::Voice* SynthEngine::findFreeOrSteal()
{
int freeIdx = -1;
int oldest = -1;
int oldestAge = -1;
for (int i = 0; i < kNumVoices; ++i)
{
if (! voices[i].active) { freeIdx = i; break; }
if (voices[i].age > oldestAge) { oldest = i; oldestAge = voices[i].age; }
}
if (freeIdx >= 0) { activeNotes++; return &voices[freeIdx]; }
voices[oldest].envState = 3;
voices[oldest].env = 0.0f;
return &voices[oldest];
}
void SynthEngine::resetVoice (Voice& v, int note, float vel)
{
v.active = true;
v.note = note;
v.velocity = vel;
v.envState = 0;
v.env = 0.0f;
v.age = 0;
v.ic1eq = 0.0;
v.ic2eq = 0.0;
double freq = 440.0 * std::pow (2.0, (note - 69) / 12.0);
double inc = freq / sampleRate;
double oct1 = paramReal (PO1OCT, params[PO1OCT]);
double oct2 = paramReal (PO2OCT, params[PO2OCT]);
double dt1 = paramReal (PO1CT, params[PO1CT]) / 1200.0;
double dt2 = paramReal (PO2CT, params[PO2CT]) / 1200.0;
double f1 = freq * std::pow (2.0, oct1 + dt1);
double f2 = freq * std::pow (2.0, oct2 + dt2);
v.inc1 = f1 / sampleRate;
v.inc2 = f2 / sampleRate;
v.glidedInc1 = v.inc1;
v.ph1 = 0.0;
v.ph2 = 0.0;
}
float SynthEngine::oscWave (int wave, double phase)
{
double ph = phase - std::floor(phase);
switch (wave)
{
case 0: return (float) std::sin (ph * 6.283185307179586);
case 1: return (float) (1.0 - 4.0 * std::fabs (ph - 0.5));
case 2: return (float) (2.0 * ph - 1.0);
case 3: return (float) (ph < 0.5 ? 1.0 : -1.0);
case 4: return (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0);
default: return 0.0f;
}
}
static float softClip (float x)
{
return std::tanh (x * 1.5f) / std::tanh(1.5f);
}
double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, double envLevel)
{
double baseHz = paramReal (PCUTOFF, (float) baseCutoff);
baseHz = std::clamp (baseHz, 20.0, 20000.0);
double mod = envAmt * envLevel * 5.0;
double hz = baseHz * std::pow (2.0, mod);
hz = std::clamp (hz, 20.0, std::min (sampleRate * 0.45, 20000.0));
return (float) hz;
}
void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
int n, double bpm, bool transportPlaying)
{
wasPlaying = transportPlaying;
lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0;
const float cutN01 = params[PCUTOFF];
const float resN01 = params[PRES];
const float fenvN01 = params[PFENV];
const float ftypeN01= params[PFTYPE];
const float atkN01 = params[PATK];
const float decN01 = params[PDEC];
const float susN01 = params[PSUS];
const float relN01 = params[PREL];
const float driveN01= params[PDRIVE];
const float glideN01= params[PGLIDE];
const float masterN01= params[PMASTER];
const float mixN01 = params[PMIX];
const float noiseN01= params[PNOISE];
const double atkSec = paramReal (PATK, atkN01);
const double decSec = paramReal (PDEC, decN01);
const double susLevel = susN01;
const double relSec = paramReal (PREL, relN01);
const double atkCoef = 1.0 - std::exp (-1.0 / (atkSec * sampleRate + 1.0));
const double decCoef = 1.0 - std::exp (-1.0 / (decSec * sampleRate + 1.0));
const double relCoef = 1.0 - std::exp (-1.0 / (relSec * sampleRate + 1.0));
const double drive = 0.1 + driveN01 * 2.4;
const double glideCoef = glideN01 < 0.001 ? 1.0 : 1.0 - std::exp (-1.0 / (glideN01 * sampleRate + 1.0));
const double master = masterN01;
const double fenvScale = fenvN01 * 5.0;
(void) fenvScale;
const float gMix = mixN01;
const float g1 = 1.0f - gMix;
const float g2 = gMix;
const float g1p = (float) std::sqrt (g1);
const float g2p = (float) std::sqrt (g2);
const int filterType = paramFilterType (ftypeN01);
bool delaySync = params[PDSYNC] > 0.5f;
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS] * 15.0f));
float delayMsParam = paramReal (PDTIME, params[PDTIME]);
float delaySec;
if (delaySync)
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
else
delaySec = delayMsParam * 0.001f;
delaySec = std::clamp (delaySec, 0.001f, 17.0f);
lastDelaySamples = delaySec * sampleRate;
const float delayFB = params[PDFB];
const float delayMix= params[PDMIX];
const float dryMix = 1.0f - delayMix;
const float wetMix = delayMix;
int delayWrite = (int) delay.writePos[0];
int delayRead = (delayWrite - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
(void) delayRead;
for (const auto& metadata : midi)
{
MidiMessage msg = metadata.getMessage();
int sm = metadata.samplePosition;
if (sm < n && msg.isNoteOn())
noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
else if (sm < n && msg.isNoteOff())
noteOff (msg.getNoteNumber());
}
float* outL = buffer.getWritePointer (0);
float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr;
for (int s = 0; s < n; ++s)
{
float sumL = 0.0f, sumR = 0.0f;
for (auto& v : voices)
{
if (! v.active) continue;
v.age++;
v.ph1 += v.inc1;
v.ph2 += v.inc2;
if (v.ph1 > 1.0) v.ph1 -= 1.0;
if (v.ph2 > 1.0) v.ph2 -= 1.0;
v.glidedInc1 = (float) ((1.0 - glideCoef) * v.glidedInc1 + glideCoef * v.inc1);
switch (v.envState)
{
case 0: v.env += (1.0 - v.env) * atkCoef; if (v.env > 0.999) v.envState = 1; break;
case 1: v.env += (susLevel - v.env) * decCoef; break;
case 2: break;
case 3: v.env += (0.0 - v.env) * relCoef; if (v.env < 0.001) { v.active = false; v.env = 0.0; activeNotes--; } break;
}
float o1 = oscWave (paramWaveIndex (params[PO1WAVE]), v.ph1);
float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2);
float raw = g1p * o1 + g2p * o2 + noiseN01 * (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0) * 0.5f;
raw *= 0.7f;
float cutoffHz = cutoffFor (v, cutN01, fenvN01, v.env);
float Q = 0.5f + resN01 * 14.0f;
float k = 1.0f / Q;
double f = cutoffHz / (float) sampleRate;
if (f > 0.49) f = 0.49;
double g = std::tan (juce::MathConstants<double>::pi * f);
double a1 = 1.0 / (1.0 + g * (g + k));
double a2 = g * a1;
double a3 = g * a2;
double vin = raw;
double v3 = vin - v.ic2eq;
double v1 = a1 * v.ic1eq + a2 * v3;
double v2 = v.ic2eq + a2 * v.ic1eq + a3 * v3;
v.ic1eq = 2.0 * v1 - v.ic1eq;
v.ic2eq = 2.0 * v2 - v.ic2eq;
float filtered;
switch (filterType)
{
case 0: filtered = (float) v2; break;
case 1: filtered = (float) v1; break;
case 2: filtered = (float) (vin - k * v1 - v2); break;
default: filtered = (float) v2; break;
}
float sig = softClip (filtered * (float) drive);
sig *= (float) (v.env * v.velocity * master);
sumL += sig;
sumR += sig;
}
float dry = sumL;
int w = delay.writePos[0];
float delayed = delay.buf[0][w];
float fb = dry + delayed * (float) delayFB;
fb = softClip (fb * 0.7f);
delay.buf[0][w] = fb;
delay.buf[1][w] = fb;
delay.writePos[0] = (w + 1) % delay.maxLen;
delay.writePos[1] = (w + 1) % delay.maxLen;
outL[s] = dryMix * dry + wetMix * delayed;
if (outR) outR[s] = outL[s];
}
}
} // namespace sm26

106
Source/SynthEngine.h Normal file
View file

@ -0,0 +1,106 @@
#pragma once
#include <JuceHeader.h>
#include "SynthParams.h"
#include "Presets.h"
namespace sm26 {
class SynthEngine
{
public:
static constexpr int kNumVoices = 12;
SynthEngine();
void prepare (double sampleRate, int maxBlockSize);
void reset();
void loadPreset (const Preset& p);
int presetIndex() const { return currentPresetIndex; }
void setPresetIndex (int i);
void setParam (int id, float n01);
float getParam (int id) const;
struct KnobView {
const char* name;
int param;
float value01;
float lo;
float hi;
};
KnobView knobView (int knob) const;
float knobValue (int knob) const;
void process (AudioBuffer<float>& buffer, MidiBuffer& midi, int n,
double bpm, bool transportPlaying);
int notesPlaying() const { return activeNotes; }
float lastBpm() const { return lastBpmValue; }
bool transport() const { return wasPlaying; }
int delayTimeSamples() const { return lastDelaySamples; }
const char* waveNameFor (int index) const;
private:
struct Voice {
bool active = false;
int note = 60;
double velocity = 0.8;
double ph1 = 0.0, ph2 = 0.0;
double inc1 = 0.0, inc2 = 0.0, glidedInc1 = 0.0;
double env = 0.0;
int envState = 0;
int age = 0;
double ic1eq = 0.0, ic2eq = 0.0;
};
void noteOn (int note, float vel);
void noteOff (int note);
void allNotesOff();
Voice* findFreeOrSteal();
void resetVoice (Voice& v, int note, float vel);
float oscWave (int wave, double phase);
float computeSingleSample (Voice& v, float& outL, float& outR,
double fad, double frel, double fsus, double fcut,
double fq, double drive, double glideCoef, double bpm);
double cutoffFor (const Voice& v, double baseCutoff, double envAmt, double env);
double sampleRate = 48000.0;
int maxBlock = 512;
float params[kNumParams];
int knobParams[kNumKnobs];
const char* knobNames[kNumKnobs];
float knobLows[kNumKnobs];
float knobHighs[kNumKnobs];
Voice voices[kNumVoices];
int activeNotes = 0;
struct DelayLine
{
std::vector<std::vector<float>> buf;
std::vector<int> writePos;
int maxLen = 1;
void prepare (double sr)
{
maxLen = (int) (sr * 18.0) + 4;
buf.assign (2, std::vector<float> (maxLen, 0.0f));
writePos.assign (2, 0);
}
void clear() { for (auto& c : buf) std::fill (c.begin(), c.end(), 0.0f); }
};
DelayLine delay;
double lastDelaySamples = 0.0;
double lastBpmValue = 120.0;
bool wasPlaying = false;
int currentPresetIndex = 0;
mutable juce::SpinLock paramLock;
};
} // namespace sm26

191
Source/SynthParams.cpp Normal file
View file

@ -0,0 +1,191 @@
#include "SynthParams.h"
namespace sm26 {
static const char* const kStepLabels[] = {
"1/32", "1/16T", "1/16", "1/8T",
"1/8", "1/4T", "1/4", "1/2T",
"1/2", "3/4", "1", "3/2",
"2", "3", "4", "8",
};
static const float kStepBeats[] = {
0.125f, 0.1667f, 0.25f, 0.3333f,
0.5f, 0.6667f, 1.0f, 1.3333f,
2.0f, 3.0f, 4.0f, 6.0f,
8.0f, 12.0f, 16.0f, 32.0f,
};
const char* const* stepsLabels() { return kStepLabels; }
const float* stepsBeats() { return kStepBeats; }
int stepsPerBeatFor(float stepIndex)
{
const int i = juce::jlimit(0, 15, (int) juce::roundToInt(stepIndex * 15.0f));
return juce::roundToInt(kStepBeats[i] * 4.0f);
}
float paramReal(int id, float n01)
{
n01 = clamp01(n01);
switch (id)
{
case PO1OCT: return -2.0f + n01 * 4.0f;
case PO1CT: return -100.0f + n01 * 200.0f;
case PO2OCT: return -2.0f + n01 * 4.0f;
case PO2CT: return -100.0f + n01 * 200.0f;
case PMIX: return n01;
case PNOISE: return n01;
case PCUTOFF: return (float) std::exp (std::log (20.0) + n01 * (std::log (20000.0) - std::log (20.0)));
case PRES: return 0.5f + n01 * 19.5f;
case PFENV: return n01;
case PATK: return (float) std::exp (std::log (0.001) + n01 * (std::log (8.0) - std::log (0.001)));
case PDEC: return (float) std::exp (std::log (0.002) + n01 * (std::log (8.0) - std::log (0.002)));
case PSUS: return n01;
case PREL: return (float) std::exp (std::log (0.002) + n01 * (std::log (8.0) - std::log (0.002)));
case PDTIME: return (float) std::exp (std::log (10.0) + n01 * (std::log (1500.0) - std::log (10.0)));
case PDFB: return n01;
case PDMIX: return n01;
case PDRIVE: return n01;
case PGLIDE: return (float) std::exp (std::log (0.001) + n01 * (std::log (2.0) - std::log (0.001)));
case PMASTER: return 0.10f + n01 * 0.70f;
default: return n01;
}
}
float unwrapParameter(int id, float n01)
{
n01 = clamp01(n01);
switch (id)
{
case PO1OCT: case PO2OCT:
return juce::jmax (0.0f, juce::jmin (1.0f, (n01 + 2.0f) / 4.0f));
case PO1CT: case PO2CT:
return (n01 + 100.0f) / 200.0f;
case PCUTOFF:
return (float) ((std::log (n01) - std::log (20.0)) / (std::log (20000.0) - std::log (20.0)));
case PATK:
return (float) ((std::log (n01) - std::log (0.001)) / (std::log (8.0) - std::log (0.001)));
case PDEC:
return (float) ((std::log (n01) - std::log (0.002)) / (std::log (8.0) - std::log (0.002)));
case PREL:
return (float) ((std::log (n01) - std::log (0.002)) / (std::log (8.0) - std::log (0.002)));
case PDTIME:
return (float) ((std::log (n01) - std::log (10.0)) / (std::log (1500.0) - std::log (10.0)));
case PGLIDE:
return (float) ((std::log (n01) - std::log (0.001)) / (std::log (2.0) - std::log (0.001)));
case PMASTER:
return (n01 - 0.10f) / 0.70f;
default:
return n01;
}
}
const char* paramShortName(int id)
{
static const char* const names[] = {
"OSC1", "OSC2", "O1-OCT", "O1-DET", "O2-OCT", "O2-DET",
"MIX", "NOISE", "CUTOFF", "RES", "F-ENV", "F-TYPE",
"ATK", "DEC", "SUS", "REL", "D-TIME", "D-STEPS",
"SYNC", "D-FB", "D-MIX", "DRIVE","GLIDE", "MASTER",
};
if (id >= 0 && id < kNumParams) return names[id];
return "----";
}
const char* paramUnit(int id)
{
switch (id)
{
case PO1OCT: case PO2OCT: return "oct";
case PO1CT: case PO2CT: return "ct";
case PCUTOFF: return "Hz";
case PDTIME: return "ms";
default: return "";
}
}
int paramWaveIndex(float n01)
{
const int i = (int) juce::roundToInt (n01 * 4.0f);
return juce::jlimit(0, 4, i);
}
int paramFilterType(float n01)
{
return juce::jlimit (0, 2, (int) juce::roundToInt (n01 * 2.0f));
}
const char* waveName(int w)
{
switch (w)
{
case 0: return "SINE";
case 1: return "TRI";
case 2: return "SAW";
case 3: return "SQRE";
case 4: return "NSE";
default: return "----";
}
}
const char* filterTypeName(int t)
{
switch (t)
{
case 0: return "LP";
case 1: return "BP";
case 2: return "HP";
default: return "LP";
}
}
String formatTimeMs(float ms)
{
if (ms >= 1000.0f) return String (ms / 1000.0, 2) + " s";
return String ((int) juce::roundToInt (ms)) + " ms";
}
String formatFreq(float hz)
{
if (hz >= 1000.0f) return String (hz / 1000.0, 2) + " kHz";
return String ((int) juce::roundToInt (hz)) + " Hz";
}
String formatPercent(float v)
{
return String ((int) juce::roundToInt (v * 100.0f)) + "%";
}
String formatParamReal(int id, float n01)
{
n01 = clamp01 (n01);
switch (id)
{
case PO1WAVE: case PO2WAVE:
return waveName (paramWaveIndex (n01));
case PO1OCT: case PO2OCT:
return String (paramReal (id, n01), 2);
case PO1CT: case PO2CT:
return String ((int) juce::roundToInt (paramReal (id, n01))) + " ct";
case PMIX: case PNOISE: case PFENV: case PSUS:
case PDFB: case PDMIX: case PDRIVE: case PMASTER:
return formatPercent (n01);
case PCUTOFF: return formatFreq (paramReal (id, n01));
case PRES: return String (paramReal (id, n01), 1);
case PFTYPE: return filterTypeName (paramFilterType (n01));
case PATK: case PDEC: case PREL:
return formatTimeMs (paramReal (id, n01) * 1000.0f);
case PDTIME: return formatTimeMs (paramReal (id, n01));
case PDSTEPS:
return stepsLabels() [juce::jlimit (0, 15, (int) juce::roundToInt (n01 * 15.0f))];
case PDSYNC:
return n01 > 0.5f ? "SYNC" : "MS";
case PGLIDE:
return formatTimeMs (paramReal (id, n01) * 1000.0f);
default:
return String ((int) juce::roundToInt (n01 * 100.0f)) + "%";
}
}
} // namespace sm26

71
Source/SynthParams.h Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include <JuceHeader.h>
#include <array>
namespace sm26 {
enum ParamId : int {
PO1WAVE = 0, PO2WAVE, PO1OCT, PO1CT, PO2OCT, PO2CT,
PMIX, PNOISE, PCUTOFF, PRES, PFENV, PFTYPE,
PATK, PDEC, PSUS, PREL,
PDTIME, PDSTEPS, PDSYNC, PDFB, PDMIX,
PDRIVE, PGLIDE, PMASTER,
kNumParams
};
static constexpr int kNumKnobs = 4;
struct ControlDef {
const char* name;
int param = 0;
float lo = 0.0f;
float hi = 1.0f;
};
struct Preset {
const char* name;
const char* category;
std::array<float, kNumParams> base;
std::array<ControlDef, kNumKnobs> ctl;
};
inline std::array<float, kNumParams> defaultParams()
{
std::array<float, kNumParams> d{};
d[PO1WAVE] = 0.0f; d[PO2WAVE] = 0.0f;
d[PO1OCT] = 0.50f; d[PO1CT] = 0.50f;
d[PO2OCT] = 0.50f; d[PO2CT] = 0.54f;
d[PMIX] = 0.18f; d[PNOISE] = 0.02f;
d[PCUTOFF] = 0.68f; d[PRES] = 0.22f;
d[PFENV] = 0.38f; d[PFTYPE] = 0.0f;
d[PATK] = 0.12f; d[PDEC] = 0.48f;
d[PSUS] = 0.50f; d[PREL] = 0.55f;
d[PDTIME] = 0.30f; d[PDSTEPS]= 6.0f;
d[PDSYNC] = 1.0f; d[PDFB] = 0.42f;
d[PDMIX] = 0.42f; d[PDRIVE] = 0.16f;
d[PGLIDE] = 0.0f; d[PMASTER]= 0.70f;
return d;
}
const char* paramShortName(int id);
const char* paramUnit(int id);
float paramReal(int id, float n01);
inline float clamp01(float v) { return v < 0.f ? 0.f : (v > 1.f ? 1.f : v); }
int paramFilterType(float n01);
String formatParamReal(int id, float n01);
int paramWaveIndex(float n01);
float unwrapParameter(int id, float n01);
String formatTimeMs(float ms);
String formatFreq(float hz);
String formatPercent(float v);
const char* waveName(int w);
const char* const* stepsLabels();
const float* stepsBeats();
int stepsPerBeatFor(float stepIndex);
} // namespace sm26