mirror of
https://codeberg.org/armin/rmx-19.git
synced 2026-10-11 16:51:55 +02:00
Initial commit: RMX-19 rack sound module (VST3/AU) with dot-matrix LCD UI
This commit is contained in:
commit
c72c0216fd
16 changed files with 2170 additions and 0 deletions
9
Source/BuildInfo.h.in
Normal file
9
Source/BuildInfo.h.in
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#define BUILD_GIT_BRANCH "@BUILD_GIT_BRANCH@"
|
||||
#define BUILD_GIT_COMMIT "@BUILD_GIT_COMMIT@"
|
||||
#define BUILD_GIT_DIRTY "@BUILD_GIT_DIRTY@"
|
||||
#define BUILD_DATE "@BUILD_DATE@"
|
||||
#define BUILD_TIME "@BUILD_TIME@"
|
||||
|
||||
#define BUILD_VERSION "1.0.0"
|
||||
576
Source/LcdPanel.cpp
Normal file
576
Source/LcdPanel.cpp
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
#include "LcdPanel.h"
|
||||
#include "BuildInfo.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5x7 dot-matrix font. Each glyph is 5 columns x 7 rows, bit 6 (0x40) of each
|
||||
// row byte is the MSB (leftmost) dot.
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
static constexpr int NW = 5, NH = 7;
|
||||
|
||||
struct Entry { int key; const juce::uint8* rows; };
|
||||
|
||||
static const juce::uint8 A_[NH] = { 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 };
|
||||
static const juce::uint8 B_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110 };
|
||||
static const juce::uint8 C_[NH] = { 0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110 };
|
||||
static const juce::uint8 D_[NH] = { 0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100 };
|
||||
static const juce::uint8 E_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111 };
|
||||
static const juce::uint8 F_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000 };
|
||||
static const juce::uint8 G_[NH] = { 0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111 };
|
||||
static const juce::uint8 H_[NH] = { 0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 };
|
||||
static const juce::uint8 I_[NH] = { 0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 };
|
||||
static const juce::uint8 J_[NH] = { 0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100 };
|
||||
static const juce::uint8 K_[NH] = { 0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 };
|
||||
static const juce::uint8 L_[NH] = { 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111 };
|
||||
static const juce::uint8 M_[NH] = { 0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001 };
|
||||
static const juce::uint8 N_[NH] = { 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001 };
|
||||
static const juce::uint8 O_[NH] = { 0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110 };
|
||||
static const juce::uint8 P_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000 };
|
||||
static const juce::uint8 Q_[NH] = { 0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101 };
|
||||
static const juce::uint8 R_[NH] = { 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001 };
|
||||
static const juce::uint8 S_[NH] = { 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110 };
|
||||
static const juce::uint8 T_[NH] = { 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 };
|
||||
static const juce::uint8 U_[NH] = { 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110 };
|
||||
static const juce::uint8 V_[NH] = { 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100 };
|
||||
static const juce::uint8 W_[NH] = { 0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001 };
|
||||
static const juce::uint8 X_[NH] = { 0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001 };
|
||||
static const juce::uint8 Y_[NH] = { 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100 };
|
||||
static const juce::uint8 Z_[NH] = { 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111 };
|
||||
static const juce::uint8 n0_[NH] = { 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110 };
|
||||
static const juce::uint8 n1_[NH] = { 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 };
|
||||
static const juce::uint8 n2_[NH] = { 0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111 };
|
||||
static const juce::uint8 n3_[NH] = { 0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110 };
|
||||
static const juce::uint8 n4_[NH] = { 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010 };
|
||||
static const juce::uint8 n5_[NH] = { 0b11111, 0b10000, 0b10000, 0b11110, 0b00001, 0b00001, 0b11110 };
|
||||
static const juce::uint8 n6_[NH] = { 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110 };
|
||||
static const juce::uint8 n7_[NH] = { 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000 };
|
||||
static const juce::uint8 n8_[NH] = { 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110 };
|
||||
static const juce::uint8 n9_[NH] = { 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100 };
|
||||
static const juce::uint8 sp_[NH] = { 0, 0, 0, 0, 0, 0, 0 };
|
||||
static const juce::uint8 ex_[NH] = { 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100 };
|
||||
static const juce::uint8 dt_[NH] = { 0, 0, 0, 0, 0, 0b00100, 0b00100 };
|
||||
static const juce::uint8 co_[NH] = { 0, 0, 0, 0, 0, 0b00110, 0b00110 };
|
||||
static const juce::uint8 cl_[NH] = { 0, 0b00100, 0b00100, 0, 0b00100, 0b00100, 0 };
|
||||
static const juce::uint8 mi_[NH] = { 0, 0, 0, 0b11111, 0, 0, 0 };
|
||||
static const juce::uint8 pl_[NH] = { 0, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0 };
|
||||
static const juce::uint8 sl_[NH] = { 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0, 0 };
|
||||
static const juce::uint8 pc_[NH] = { 0b10100, 0b10100, 0b01000, 0b10100, 0b00101, 0b00101, 0b00010 };
|
||||
static const juce::uint8 ap_[NH] = { 0b00100, 0b00100, 0b00100, 0, 0, 0, 0 };
|
||||
static const juce::uint8 lt_[NH] = { 0b00011, 0b00110, 0b01100, 0b11000, 0b01100, 0b00110, 0b00011 };
|
||||
static const juce::uint8 gt_[NH] = { 0b11000, 0b01100, 0b00110, 0b00011, 0b00110, 0b01100, 0b11000 };
|
||||
static const juce::uint8 eq_[NH] = { 0, 0, 0b11111, 0, 0b11111, 0, 0 };
|
||||
static const juce::uint8 hs_[NH] = { 0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010 };
|
||||
|
||||
static const Entry table[] =
|
||||
{
|
||||
{ 'A', A_ }, { 'B', B_ }, { 'C', C_ }, { 'D', D_ }, { 'E', E_ },
|
||||
{ 'F', F_ }, { 'G', G_ }, { 'H', H_ }, { 'I', I_ }, { 'J', J_ },
|
||||
{ 'K', K_ }, { 'L', L_ }, { 'M', M_ }, { 'N', N_ }, { 'O', O_ },
|
||||
{ 'P', P_ }, { 'Q', Q_ }, { 'R', R_ }, { 'S', S_ }, { 'T', T_ },
|
||||
{ 'U', U_ }, { 'V', V_ }, { 'W', W_ }, { 'X', X_ }, { 'Y', Y_ },
|
||||
{ 'Z', Z_ }, { '0', n0_ }, { '1', n1_ }, { '2', n2_ }, { '3', n3_ },
|
||||
{ '4', n4_ }, { '5', n5_ }, { '6', n6_ }, { '7', n7_ }, { '8', n8_ },
|
||||
{ '9', n9_ }, { ' ', sp_ }, { '.', dt_ }, { ',', co_ }, { ':', cl_ },
|
||||
{ '-', mi_ }, { '+', pl_ }, { '/', sl_ }, { '%', pc_ }, { '\'', ap_ },
|
||||
{ '<', lt_ }, { '>', gt_ }, { '=', eq_ }, { '#', hs_ }, { '!', ex_ },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const juce::uint8* LcdPanel::glyphFor (int key)
|
||||
{
|
||||
if (key >= 'a' && key <= 'z') key = key - 'a' + 'A';
|
||||
for (const auto& e : table)
|
||||
if (e.key == key) return e.rows;
|
||||
return sp_;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LcdPanel::LcdPanel (RMX19AudioProcessor& p)
|
||||
: proc (p)
|
||||
{
|
||||
dotImage = juce::Image (juce::Image::ARGB, gridW, gridH, true);
|
||||
splashUntil = juce::Time::getMillisecondCounter() + 1500;
|
||||
lastMidi = proc.getLastMidiTime();
|
||||
startTimerHz (22);
|
||||
}
|
||||
|
||||
LcdPanel::~LcdPanel()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void LcdPanel::timerCallback()
|
||||
{
|
||||
float l, r; proc.getMeters (l, r);
|
||||
meterL = std::max (l, meterL * 0.5f);
|
||||
meterR = std::max (r, meterR * 0.5f);
|
||||
|
||||
if (proc.getLastMidiTime() != lastMidi)
|
||||
{
|
||||
lastMidi = proc.getLastMidiTime();
|
||||
powerGlow = 1.0f;
|
||||
}
|
||||
powerGlow = std::max (0.0f, powerGlow - 0.04f);
|
||||
|
||||
if (screen == Screen::Splash && juce::Time::getMillisecondCounter() > splashUntil)
|
||||
screen = Screen::Home;
|
||||
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void LcdPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds();
|
||||
|
||||
// recessed bezel
|
||||
g.setColour (juce::Colour (0xff0c0a08));
|
||||
g.fillRect (b);
|
||||
g.setColour (juce::Colour (0xff4a453c));
|
||||
g.drawRect (b, 2);
|
||||
|
||||
// inner LCD glass (black)
|
||||
const float scale = (float) b.getWidth() / (float) gridW;
|
||||
const int cell = 2;
|
||||
const int w = gridW * cell;
|
||||
const int h = gridH * cell;
|
||||
const int ox = (b.getWidth() - w) / 2;
|
||||
const int oy = (b.getHeight() - h) / 2 + 2;
|
||||
g.setColour (juce::Colour (0xff020703));
|
||||
g.fillRect (ox, oy, w, h);
|
||||
|
||||
{
|
||||
juce::Graphics dg (dotImage);
|
||||
dg.fillAll (juce::Colours::black);
|
||||
drawContent (dg);
|
||||
}
|
||||
|
||||
for (int y = 0; y < gridH; ++y)
|
||||
{
|
||||
for (int x = 0; x < gridW; ++x)
|
||||
{
|
||||
const auto px = dotImage.getPixelAt (x, y);
|
||||
const float lvl = px.getAlpha() / 255.0f;
|
||||
if (lvl <= 0.0f) continue;
|
||||
const juce::Colour c = juce::Colour::fromFloatRGBA (0.04f + 0.02f * powerGlow,
|
||||
0.96f * lvl, 0.25f * lvl, 1.0f);
|
||||
g.setColour (c);
|
||||
g.fillRect (ox + x * cell, oy + y * cell, cell, cell);
|
||||
}
|
||||
}
|
||||
(void) scale;
|
||||
}
|
||||
|
||||
void LcdPanel::drawContent (juce::Graphics& g)
|
||||
{
|
||||
switch (screen)
|
||||
{
|
||||
case Screen::Splash: drawSplash (g); break;
|
||||
case Screen::Home: drawHome (g); break;
|
||||
case Screen::Menu: drawMenu (g); break;
|
||||
case Screen::Patch: drawPatchList (g); break;
|
||||
case Screen::Param: drawParamScreen (g); break;
|
||||
case Screen::System: drawSystem (g); break;
|
||||
case Screen::Info: drawInfo (g); break;
|
||||
}
|
||||
drawFooter (g);
|
||||
}
|
||||
|
||||
void LcdPanel::px (juce::Graphics& g, int x, int y, float intensity)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= gridW || y >= gridH) return;
|
||||
if (intensity <= 0.0f) return;
|
||||
const int a = (int) (255.0f * juce::jlimit (0.0f, 1.0f, intensity));
|
||||
g.setColour (juce::Colour::fromRGBA (255, 255, 255, a));
|
||||
g.fillRect (x, y, 1, 1);
|
||||
}
|
||||
|
||||
void LcdPanel::pxText (juce::Graphics& g, int x, int y, const juce::String& text, float intensity)
|
||||
{
|
||||
int cx = x;
|
||||
for (int i = 0; i < text.length(); ++i)
|
||||
{
|
||||
const int key = text[i];
|
||||
const juce::uint8* rows = glyphFor (key);
|
||||
for (int row = 0; row < NH; ++row)
|
||||
{
|
||||
const juce::uint8 bits = rows[row];
|
||||
for (int col = 0; col < NW; ++col)
|
||||
if (bits & (1u << (NW - 1 - col)))
|
||||
px (g, cx + col, y + row, intensity);
|
||||
}
|
||||
cx += NW + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void LcdPanel::drawFilled (juce::Graphics& g, int x, int y, int w, int h, float intensity)
|
||||
{
|
||||
for (int yy = 0; yy < h; ++yy)
|
||||
for (int xx = 0; xx < w; ++xx)
|
||||
px (g, x + xx, y + yy, intensity);
|
||||
}
|
||||
|
||||
void LcdPanel::drawHBar (juce::Graphics& g, int x, int y, int w, double level, float intensity)
|
||||
{
|
||||
const int filled = juce::roundToInt (w * juce::jlimit (0.0, 1.0, level));
|
||||
if (filled > 0) drawFilled (g, x, y, filled, 1, intensity);
|
||||
}
|
||||
|
||||
void LcdPanel::drawFooter (juce::Graphics& g)
|
||||
{
|
||||
juce::String branch = juce::String (BUILD_GIT_BRANCH);
|
||||
if (branch.length() > 6) branch = branch.substring (0, 6);
|
||||
const juce::String git = branch + "@" + juce::String (BUILD_GIT_COMMIT) + juce::String (BUILD_GIT_DIRTY);
|
||||
const juce::String dt = juce::String (BUILD_DATE).substring (5) + " " + juce::String (BUILD_TIME);
|
||||
|
||||
drawFilled (g, 0, 61, gridW, 1, 0.4f);
|
||||
pxText (g, 2, 63, git, 0.75f);
|
||||
|
||||
const auto w = dt.length() * (NW + 1) - 1;
|
||||
pxText (g, gridW - w - 2, 63, dt, 0.75f);
|
||||
|
||||
(void) powerGlow;
|
||||
}
|
||||
|
||||
void LcdPanel::drawSplash (juce::Graphics& g)
|
||||
{
|
||||
pxText (g, 46, 6, "* RMX-19 *", 1.0f);
|
||||
pxText (g, 30, 18, "RACK SYNTHESIZER", 0.9f);
|
||||
drawFilled (g, 10, 30, gridW - 20, 1, 0.5f);
|
||||
pxText (g, 44, 34, "INITIALIZING", 0.8f);
|
||||
pxText (g, 34, 45, "DSP READY", 0.75f);
|
||||
pxText (g, 56, 56, "REV.B", 0.6f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawHome (juce::Graphics& g)
|
||||
{
|
||||
const int patch = proc.getPatchIndex();
|
||||
const auto& p = proc.getBank().get (patch);
|
||||
|
||||
const juce::String num = juce::String (patch + 1).paddedLeft ('0', 2);
|
||||
pxText (g, 2, 3, num, 1.0f);
|
||||
pxText (g, 4, 15, p.name, 1.0f);
|
||||
pxText (g, 4, 28, p.category, 0.8f);
|
||||
|
||||
drawFilled (g, 4, 42, 88, 1, 0.5f);
|
||||
drawHBar (g, 4, 43, 88, proc.getActiveVoices() / 16.0, 0.9f);
|
||||
|
||||
pxText (g, 116, 42, "L", 0.6f);
|
||||
pxText (g, 138, 42, "R", 0.6f);
|
||||
drawFilled (g, 122, 40, 1, 8, 0.5f);
|
||||
drawHBar (g, 123, 41, 12, meterL, 1.0f);
|
||||
drawHBar (g, 123, 44, 12, meterR, 1.0f);
|
||||
drawFilled (g, 123, 40, 12, 1, 0.4f);
|
||||
drawFilled (g, 123, 47, 12, 1, 0.4f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawMenu (juce::Graphics& g)
|
||||
{
|
||||
static const char* items[] = {
|
||||
"PATCH SELECT", "OSC SELECT", "FILTER SETUP", "AMP ENVELOPE",
|
||||
"FILT ENVELOPE", "MODULATION", "EFFECTS", "MASTER", "SYSTEM", "ABOUT"
|
||||
};
|
||||
const int rows = (int) juce::numElementsInArray (items);
|
||||
const int visible = 6;
|
||||
const int top = juce::jmax (0, juce::jmin (cursor - 3, rows - visible));
|
||||
for (int i = 0; i < visible; ++i)
|
||||
{
|
||||
const int idx = top + i;
|
||||
if (idx >= rows) break;
|
||||
const int y = 2 + i * 10 + 1;
|
||||
const bool sel = (idx == cursor);
|
||||
pxText (g, 4, y, items[idx], sel ? 1.0f : 0.6f);
|
||||
if (sel)
|
||||
{
|
||||
drawFilled (g, 1, y, 1, NH, 1.0f);
|
||||
pxText (g, 142, y, ">", 1.0f);
|
||||
}
|
||||
}
|
||||
drawFilled (g, 151, 8, 1, 48, 0.4f);
|
||||
const double frac = rows > 1 ? cursor / (double) (rows - 1) : 0.0;
|
||||
const int thumbY = 8 + (int) (frac * 40.0);
|
||||
drawFilled (g, 150, thumbY - 2, 3, 4, 0.8f);
|
||||
pxText (g, 4, 62, "UP/DN MOVE OK OPEN", 0.55f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawPatchList (juce::Graphics& g)
|
||||
{
|
||||
const int bankSize = proc.getBank().size();
|
||||
const int visible = 6;
|
||||
const int top = juce::jmax (0, juce::jmin (patchCursor - 2, bankSize - visible));
|
||||
for (int i = 0; i < visible; ++i)
|
||||
{
|
||||
const int idx = top + i;
|
||||
if (idx >= bankSize) break;
|
||||
const auto& p = proc.getBank().get (idx);
|
||||
const int y = 2 + i * 10 + 1;
|
||||
const bool sel = (idx == patchCursor);
|
||||
pxText (g, 4, y, juce::String (idx + 1).paddedLeft ('0', 2) + " " + juce::String (p.name),
|
||||
sel ? 1.0f : 0.6f);
|
||||
if (sel) drawFilled (g, 1, y, 1, NH, 1.0f);
|
||||
if (idx == proc.getPatchIndex()) pxText (g, 138, y, "*", 0.9f);
|
||||
}
|
||||
drawFilled (g, 140, 8, 1, 48, 0.4f);
|
||||
const double frac = bankSize > 1 ? patchCursor / (double) (bankSize - 1) : 0.0;
|
||||
const int thumbY = 8 + (int) (frac * 40.0);
|
||||
drawFilled (g, 139, thumbY - 2, 3, 4, 0.8f);
|
||||
pxText (g, 4, 62, "UP/DN BROWSE OK LOAD", 0.55f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawParamScreen (juce::Graphics& g)
|
||||
{
|
||||
pxText (g, 2, 2, paramScreenTitle (paramScreenIdx), 1.0f);
|
||||
drawFilled (g, 2, 9, gridW - 4, 1, 0.35f);
|
||||
|
||||
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune, prm::Id::oscMix };
|
||||
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
|
||||
static const prm::Id ampEnv[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR };
|
||||
static const prm::Id fltEnv[] = { prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
|
||||
static const prm::Id mod[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
|
||||
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
|
||||
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune };
|
||||
|
||||
const prm::Id* rows = nullptr;
|
||||
int count = 0;
|
||||
switch (paramScreenIdx)
|
||||
{
|
||||
case 0: rows = voice; count = (int) juce::numElementsInArray (voice); break;
|
||||
case 1: rows = filter; count = (int) juce::numElementsInArray (filter); break;
|
||||
case 2: rows = ampEnv; count = (int) juce::numElementsInArray (ampEnv); break;
|
||||
case 3: rows = fltEnv; count = (int) juce::numElementsInArray (fltEnv); break;
|
||||
case 4: rows = mod; count = (int) juce::numElementsInArray (mod); break;
|
||||
case 5: rows = fx; count = (int) juce::numElementsInArray (fx); break;
|
||||
case 6: rows = master; count = (int) juce::numElementsInArray (master); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
const int visible = 5;
|
||||
const int top = juce::jmax (0, juce::jmin (cursor - 2, count - visible));
|
||||
for (int i = 0; i < visible; ++i)
|
||||
{
|
||||
const int ri = top + i;
|
||||
if (ri >= count) break;
|
||||
const prm::Id id = rows[ri];
|
||||
const auto& d = prm::desc (id);
|
||||
const float cur = proc.apvts.getRawParameterValue (d.id)->load();
|
||||
const auto* par = proc.apvts.getParameter (d.id);
|
||||
const float v01 = par->convertTo0to1 (cur);
|
||||
|
||||
const int y = 12 + i * 10;
|
||||
const bool sel = (ri == cursor);
|
||||
pxText (g, 3, y, d.shortName, 0.55f);
|
||||
pxText (g, 22, y, prm::formatParam (id, cur), sel ? 1.0f : 0.7f);
|
||||
drawFilled (g, 22, y + 6, 130, 1, 0.15f);
|
||||
drawHBar (g, 22, y + 6, 130, v01, sel ? 1.0f : 0.5f);
|
||||
if (sel)
|
||||
{
|
||||
drawFilled (g, 0, y, 1, NH, 1.0f);
|
||||
pxText (g, 148, y, "<>", 1.0f);
|
||||
}
|
||||
}
|
||||
pxText (g, 4, 62, "LEFT/RIGHT EDIT OK BACK", 0.55f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawSystem (juce::Graphics& g)
|
||||
{
|
||||
static const char* items[] = { "FACTORY INIT", "ALL NOTES OFF", "MIDI TEST", "BACK" };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
const int y = 2 + i * 10 + 1;
|
||||
const bool sel = (i == cursor);
|
||||
pxText (g, 4, y, items[i], sel ? 1.0f : 0.6f);
|
||||
if (sel) drawFilled (g, 1, y, 1, NH, 1.0f);
|
||||
}
|
||||
if (cursor == 2 && powerGlow > 0.5f)
|
||||
pxText (g, 4, 50, "MIDI RX " + juce::String (lastMidi ? "ACTIVE" : "IDLE"), 0.6f);
|
||||
}
|
||||
|
||||
void LcdPanel::drawInfo (juce::Graphics& g)
|
||||
{
|
||||
pxText (g, 2, 3, "RMX-19 RACK SYNTH", 0.9f);
|
||||
pxText (g, 2, 15, "GIT " + juce::String (BUILD_GIT_BRANCH) + " " + BUILD_GIT_COMMIT + BUILD_GIT_DIRTY, 0.85f);
|
||||
pxText (g, 2, 27, "BUILT " + juce::String (BUILD_DATE) + " " + BUILD_TIME, 0.85f);
|
||||
pxText (g, 2, 39, "ARM64 JUCE VERSION " + juce::String (JUCE_MAJOR_VERSION) + "." + juce::String (JUCE_MINOR_VERSION), 0.7f);
|
||||
pxText (g, 2, 51, "MIDI NOTE + CC CONTROL", 0.7f);
|
||||
}
|
||||
|
||||
// ---- editing helpers ------------------------------------------------------
|
||||
|
||||
int LcdPanel::rowCountForScreen() const
|
||||
{
|
||||
switch (screen)
|
||||
{
|
||||
case Screen::Menu: return 10;
|
||||
case Screen::System: return 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int LcdPanel::paramCountForScreen (int idx) const
|
||||
{
|
||||
switch (idx)
|
||||
{
|
||||
case 0: return 4;
|
||||
case 1: return 5;
|
||||
case 2: return 4;
|
||||
case 3: return 4;
|
||||
case 4: return 4;
|
||||
case 5: return 3;
|
||||
case 6: return 5;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const char* LcdPanel::paramScreenTitle (int idx) const
|
||||
{
|
||||
switch (idx)
|
||||
{
|
||||
case 0: return "OSCILLATOR";
|
||||
case 1: return "FILTER";
|
||||
case 2: return "AMP ENVELOPE";
|
||||
case 3: return "FILT ENVELOPE";
|
||||
case 4: return "MODULATION";
|
||||
case 5: return "EFFECTS";
|
||||
case 6: return "MASTER";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
void LcdPanel::openParamScreen (int idx)
|
||||
{
|
||||
paramScreenIdx = juce::jlimit (0, 6, idx);
|
||||
cursor = 0;
|
||||
enterScreen (Screen::Param);
|
||||
}
|
||||
|
||||
void LcdPanel::moveCursor (int d)
|
||||
{
|
||||
if (screen == Screen::Patch)
|
||||
{
|
||||
patchCursor = juce::jlimit (0, proc.getBank().size() - 1, patchCursor + d);
|
||||
setDirty();
|
||||
}
|
||||
else if (screen == Screen::Param)
|
||||
{
|
||||
cursor = juce::jlimit (0, paramCountForScreen (paramScreenIdx) - 1, cursor + d);
|
||||
setDirty();
|
||||
}
|
||||
else if (rowCountForScreen() > 0)
|
||||
{
|
||||
cursor = juce::jlimit (0, rowCountForScreen() - 1, cursor + d);
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void LcdPanel::stepParam (prm::Id id, int dir, bool coarse)
|
||||
{
|
||||
auto* par = dynamic_cast<juce::AudioParameterFloat*> (proc.apvts.getParameter (prm::desc (id).id));
|
||||
if (par == nullptr) return;
|
||||
|
||||
const float cur = proc.apvts.getRawParameterValue (prm::desc (id).id)->load();
|
||||
const float nv = prm::changeParam (id, cur, dir, coarse);
|
||||
if (nv != cur)
|
||||
par->setValueNotifyingHost (par->convertTo0to1 (nv));
|
||||
setDirty();
|
||||
}
|
||||
|
||||
static prm::Id paramForCursor (int screenIdx, int row)
|
||||
{
|
||||
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune, prm::Id::oscMix };
|
||||
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
|
||||
static const prm::Id ampEnv[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR };
|
||||
static const prm::Id fltEnv[] = { prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
|
||||
static const prm::Id mod[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
|
||||
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
|
||||
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune };
|
||||
|
||||
switch (screenIdx)
|
||||
{
|
||||
case 0: return voice[row];
|
||||
case 1: return filter[row];
|
||||
case 2: return ampEnv[row];
|
||||
case 3: return fltEnv[row];
|
||||
case 4: return mod[row];
|
||||
case 5: return fx[row];
|
||||
case 6: return master[row];
|
||||
default: return prm::Id::vol;
|
||||
}
|
||||
}
|
||||
|
||||
void LcdPanel::adjustCurrent (int dir, bool coarse)
|
||||
{
|
||||
if (screen == Screen::Param)
|
||||
stepParam (paramForCursor (paramScreenIdx, cursor), dir, coarse);
|
||||
}
|
||||
|
||||
void LcdPanel::navUp() { moveCursor (-1); }
|
||||
void LcdPanel::navDown() { moveCursor (1); }
|
||||
void LcdPanel::navLeft() { adjustCurrent (-1, false); }
|
||||
void LcdPanel::navRight() { adjustCurrent (1, false); }
|
||||
|
||||
void LcdPanel::navEnter()
|
||||
{
|
||||
switch (screen)
|
||||
{
|
||||
case Screen::Splash: enterScreen (Screen::Home); break;
|
||||
case Screen::Home: enterScreen (Screen::Menu); break;
|
||||
case Screen::Menu:
|
||||
switch (cursor)
|
||||
{
|
||||
case 0: enterScreen (Screen::Patch); break;
|
||||
case 1: openParamScreen (0); break;
|
||||
case 2: openParamScreen (1); break;
|
||||
case 3: openParamScreen (2); break;
|
||||
case 4: openParamScreen (3); break;
|
||||
case 5: openParamScreen (4); break;
|
||||
case 6: openParamScreen (5); break;
|
||||
case 7: openParamScreen (6); break;
|
||||
case 8: enterScreen (Screen::System); break;
|
||||
case 9: enterScreen (Screen::Info); break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case Screen::Patch:
|
||||
proc.selectPatch (patchCursor);
|
||||
enterScreen (Screen::Home);
|
||||
break;
|
||||
case Screen::System:
|
||||
switch (cursor)
|
||||
{
|
||||
case 0: proc.getBank().writeTo (0, proc.apvts); proc.selectPatch (0); break;
|
||||
case 1: proc.getEngine().allNotesOff (true); break;
|
||||
case 2: break;
|
||||
case 3: enterScreen (Screen::Home); break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case Screen::Info:
|
||||
case Screen::Param:
|
||||
default:
|
||||
enterScreen (Screen::Home);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LcdPanel::navExit()
|
||||
{
|
||||
if (screen == Screen::Menu)
|
||||
enterScreen (Screen::Home);
|
||||
else if (screen != Screen::Home)
|
||||
enterScreen (Screen::Menu);
|
||||
}
|
||||
|
||||
bool LcdPanel::keyTrap (const juce::KeyPress& k)
|
||||
{
|
||||
if (k.isKeyCode (juce::KeyPress::upKey)) { navUp(); return true; }
|
||||
if (k.isKeyCode (juce::KeyPress::downKey)) { navDown(); return true; }
|
||||
if (k.isKeyCode (juce::KeyPress::leftKey)) { navLeft(); return true; }
|
||||
if (k.isKeyCode (juce::KeyPress::rightKey)) { navRight(); return true; }
|
||||
if (k.isKeyCode (juce::KeyPress::returnKey) || k.getTextCharacter() == ' ')
|
||||
{ navEnter(); return true; }
|
||||
if (k.isKeyCode (juce::KeyPress::escapeKey))
|
||||
{ navExit(); return true; }
|
||||
return false;
|
||||
}
|
||||
75
Source/LcdPanel.h
Normal file
75
Source/LcdPanel.h
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
#include "Params.h"
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
class LcdPanel : public juce::Component, public juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit LcdPanel (RMX19AudioProcessor& proc);
|
||||
~LcdPanel() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
|
||||
void navUp(); void navDown();
|
||||
void navLeft(); void navRight();
|
||||
void navEnter(); void navExit();
|
||||
|
||||
void resetToHome();
|
||||
|
||||
static constexpr int gridW = 160; // dot-matrix cell resolution
|
||||
static constexpr int gridH = 64;
|
||||
|
||||
bool keyTrap (const juce::KeyPress&);
|
||||
|
||||
private:
|
||||
enum class Screen { Splash, Home, Menu, Patch, Param, System, Info };
|
||||
|
||||
RMX19AudioProcessor& proc;
|
||||
|
||||
Screen screen = Screen::Splash;
|
||||
juce::uint32 splashUntil = 0;
|
||||
|
||||
int cursor = 0; // menu / patch / param cursor
|
||||
int patchCursor = 0;
|
||||
int paramScreenIdx = 0; // which param screen is open
|
||||
|
||||
float meterL = 0.0f, meterR = 0.0f;
|
||||
float powerGlow = 0.0f;
|
||||
juce::uint32 lastMidi = 0;
|
||||
|
||||
juce::Image dotImage;
|
||||
|
||||
static const juce::uint8* glyphFor (int key);
|
||||
|
||||
void drawContent (juce::Graphics&);
|
||||
void px (juce::Graphics&, int x, int y, float intensity);
|
||||
void pxText (juce::Graphics&, int x, int y, const juce::String&, float intensity);
|
||||
void drawFilled (juce::Graphics&, int x, int y, int w, int h, float intensity);
|
||||
void drawHBar (juce::Graphics&, int x, int y, int w, double level, float intensity);
|
||||
|
||||
void drawSplash (juce::Graphics&);
|
||||
void drawHome (juce::Graphics&);
|
||||
void drawMenu (juce::Graphics&);
|
||||
void drawPatchList (juce::Graphics&);
|
||||
void drawParamScreen (juce::Graphics&);
|
||||
void drawSystem (juce::Graphics&);
|
||||
void drawInfo (juce::Graphics&);
|
||||
void drawFooter (juce::Graphics&);
|
||||
|
||||
void stepParam (prm::Id id, int dir, bool coarse);
|
||||
|
||||
void setDirty() { dotImage.clear (dotImage.getBounds()); repaint(); }
|
||||
void enterScreen (Screen s) { screen = s; cursor = 0; setDirty(); }
|
||||
|
||||
void timerCallback() override;
|
||||
|
||||
int rowCountForScreen() const;
|
||||
void moveCursor (int d);
|
||||
void adjustCurrent (int dir, bool coarse);
|
||||
void openParamScreen (int idx);
|
||||
const char* paramScreenTitle (int idx) const;
|
||||
int paramCountForScreen (int idx) const;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LcdPanel)
|
||||
};
|
||||
154
Source/Params.cpp
Normal file
154
Source/Params.cpp
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#include "Params.h"
|
||||
#include "PresetBank.h"
|
||||
|
||||
namespace prm {
|
||||
|
||||
static const Desc table[num] = {
|
||||
// id name short min max def disc fine coarse
|
||||
{ "osc1wave", "OSC1 WAVE", "O1WV", 0, 4, 2, true, 1, 1 },
|
||||
{ "osc2wave", "OSC2 WAVE", "O2WV", 0, 4, 3, true, 1, 1 },
|
||||
{ "detune", "DETUNE", "DTN", -50, 50, 8, false, 1, 6 },
|
||||
{ "oscmix", "OSC BAL", "MIX", 0, 100, 52, false, 1, 8 },
|
||||
{ "flttype", "FILTER TYPE", "FTYP", 0, 3, 1, true, 1, 1 },
|
||||
{ "fltcut", "CUTOFF", "CUT", 20, 20000, 3400, false, 20, 400 },
|
||||
{ "fltres", "RESONANCE", "RES", 0, 0.98f, 0.18f, false, 0.01f, 0.08f },
|
||||
{ "fltenv", "FILT ENV", "FEN", 0, 100, 52, false, 1, 8 },
|
||||
{ "fltkey", "KEY TRACK", "KEY", 0, 100, 40, false, 1, 8 },
|
||||
{ "ampA", "AMP ATTACK", "AAT", 0.001f, 6, 0.008f, false, 0.001f, 0.05f },
|
||||
{ "ampD", "AMP DECAY", "ADE", 0.001f, 6, 0.26f, false, 0.001f, 0.05f },
|
||||
{ "ampS", "AMP SUSTAIN", "ASUS", 0, 1, 0.70f, false, 0.01f, 0.08f },
|
||||
{ "ampR", "AMP RELEASE", "AREL", 0.001f, 8, 0.35f, false, 0.001f, 0.05f },
|
||||
{ "fenvA", "FENV ATTACK", "FAT", 0.001f, 6, 0.02f, false, 0.001f, 0.05f },
|
||||
{ "fenvD", "FENV DECAY", "FDE", 0.001f, 6, 0.16f, false, 0.001f, 0.05f },
|
||||
{ "fenvS", "FENV SUSTAIN", "FSUS", 0, 1, 0.26f, false, 0.01f, 0.08f },
|
||||
{ "fenvR", "FENV RELEASE", "FRE", 0.001f, 8, 0.25f, false, 0.001f, 0.05f },
|
||||
{ "lforate", "LFO RATE", "LFR", 0.02f, 22, 4.2f, false, 0.02f, 0.5f },
|
||||
{ "lfoshape", "LFO SHAPE", "LFS", 0, 3, 0, true, 1, 1 },
|
||||
{ "lfodepth", "LFO DEPTH", "LFD", 0, 1, 0.22f, false, 0.01f, 0.08f },
|
||||
{ "lfotarget", "LFO TARGET", "LFT", 0, 2, 0, true, 1, 1 },
|
||||
{ "fxtype", "FX TYPE", "FXT", 0, 3, 1, true, 1, 1 },
|
||||
{ "fxamt", "FX DEPTH", "FXD", 0, 1, 0.30f, false, 0.01f, 0.08f },
|
||||
{ "fxtime", "FX TIME", "FXT", 0.05f, 1.2f, 0.28f, false, 0.01f, 0.12f },
|
||||
{ "vol", "MASTER VOL", "VOL", 0, 1, 0.85f, false, 0.01f, 0.08f },
|
||||
{ "pan", "STEREO PAN", "PAN", -1, 1, 0, false, 0.02f, 0.2f },
|
||||
{ "poly", "POLYPHONY", "POLY", 1, 16, 10, true, 1, 1 },
|
||||
{ "glide", "PORTAMENTO", "GLD", 0, 2, 0, false, 0.02f, 0.2f },
|
||||
{ "tune", "MASTER TUNE", "TUN", -12, 12, 0, true, 1, 1 },
|
||||
};
|
||||
|
||||
const Desc& desc (Id id) { return table[static_cast<int>(id)]; }
|
||||
bool validId (int i) { return i >= 0 && i < num; }
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout createLayout()
|
||||
{
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
|
||||
PresetBank bank;
|
||||
const Patch& p0 = bank.get (0); // seed APVTS defaults with the first patch
|
||||
|
||||
for (int i = 0; i < num; ++i)
|
||||
{
|
||||
const Desc& d = table[i];
|
||||
juce::NormalisableRange<float> r (d.min, d.max);
|
||||
if (juce::String (d.id) == "fltcut")
|
||||
r = juce::NormalisableRange<float> (d.min, d.max, 1.0f, 0.22f);
|
||||
else if (d.discrete)
|
||||
r = juce::NormalisableRange<float> (d.min, d.max, 1.0f);
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||
d.id, d.name, r, p0.v[i]));
|
||||
}
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> ("patchsel", "Patch", 0, 15, 0));
|
||||
return layout;
|
||||
}
|
||||
|
||||
const char* waveName (int w)
|
||||
{
|
||||
switch (w) { case 0: return "SINE"; case 1: return "TRIANG"; case 2: return "SAWTOOTH";
|
||||
case 3: return "PULSE"; case 4: return "NOISE"; default: return "-----"; }
|
||||
}
|
||||
|
||||
const char* fltTypeName (int t)
|
||||
{
|
||||
switch (t) { case 0: return "OFF "; case 1: return "LOWPASS "; case 2: return "BANDPASS ";
|
||||
case 3: return "HIGH-PASS"; default: return "OFF "; }
|
||||
}
|
||||
|
||||
const char* lfoShapeName (int s)
|
||||
{
|
||||
switch (s) { case 0: return "SINE"; case 1: return "TRIANG"; case 2: return "SQUARE";
|
||||
case 3: return "S+H "; default: return "SINE"; }
|
||||
}
|
||||
|
||||
const char* lfoTargetName (int t)
|
||||
{
|
||||
switch (t) { case 0: return "FILTER"; case 1: return "PITCH"; case 2: return "AMP ";
|
||||
default: return "FILTER"; }
|
||||
}
|
||||
|
||||
const char* fxTypeName (int t)
|
||||
{
|
||||
switch (t) { case 0: return "OFF"; case 1: return "CHORUS"; case 2: return "DELAY ";
|
||||
case 3: return "SPACE"; default: return "OFF"; }
|
||||
}
|
||||
|
||||
static juce::String fmtTime (float t)
|
||||
{
|
||||
if (t >= 1.0f) return juce::String (t, 2) + "s";
|
||||
if (t >= 0.1f) return juce::String (t, 2) + "s";
|
||||
return juce::String ((int) (t * 1000.0f)) + "ms";
|
||||
}
|
||||
|
||||
static juce::String fmtPercent (float v) { return juce::String ((int) (v * 100.0f + 0.5f)) + "%"; }
|
||||
|
||||
juce::String formatParam (Id id, float v)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case Id::osc1Wave:
|
||||
case Id::osc2Wave: return waveName ((int) v);
|
||||
case Id::detune: return juce::String ((int) v > 0 ? "+" : "") + juce::String ((int) v) + "c";
|
||||
case Id::oscMix: return juce::String ((int) (100.0f - v)) + "/" + juce::String ((int) v);
|
||||
case Id::fltType: return fltTypeName ((int) v);
|
||||
case Id::fltCutoff:
|
||||
if (v >= 1000.0f) return juce::String (v / 1000.0f, 2) + "kHz";
|
||||
return juce::String ((int) v) + "Hz";
|
||||
case Id::fltRes: return fmtPercent (v);
|
||||
case Id::fltEnv: return fmtPercent (v / 100.0f);
|
||||
case Id::fltKey: return fmtPercent (v / 100.0f);
|
||||
case Id::ampA: case Id::ampD: case Id::ampR:
|
||||
case Id::fenvA: case Id::fenvD: case Id::fenvR: return fmtTime (v);
|
||||
case Id::ampS: case Id::fenvS: return fmtPercent (v);
|
||||
case Id::lfoRate: return juce::String (v, 2) + "Hz";
|
||||
case Id::lfoShape: return lfoShapeName ((int) v);
|
||||
case Id::lfoDepth: return fmtPercent (v);
|
||||
case Id::lfoTarget:return lfoTargetName ((int) v);
|
||||
case Id::fxType: return fxTypeName ((int) v);
|
||||
case Id::fxAmt: return fmtPercent (v);
|
||||
case Id::fxTime: return fmtTime (v);
|
||||
case Id::vol: return fmtPercent (v);
|
||||
case Id::pan:
|
||||
if (v < -0.02f) return "L" + juce::String ((int) (v * -100.0f));
|
||||
if (v > 0.02f) return "R" + juce::String ((int) (v * 100.0f));
|
||||
return "CTR";
|
||||
case Id::poly: return juce::String ((int) v) + " VOICE";
|
||||
case Id::glide: return v < 0.001f ? juce::String ("0") : fmtTime (v);
|
||||
case Id::tune: {
|
||||
const int t = (int) v;
|
||||
return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST";
|
||||
}
|
||||
default: return "-----";
|
||||
}
|
||||
}
|
||||
|
||||
float changeParam (Id id, float cur, int dir, bool coarse)
|
||||
{
|
||||
const Desc& d = desc (id);
|
||||
float step = coarse ? d.coarse : d.fine;
|
||||
cur = juce::jlimit (d.min, d.max, cur + dir * step);
|
||||
if (d.discrete) cur = (float) juce::roundToInt (cur);
|
||||
return cur;
|
||||
}
|
||||
|
||||
} // namespace prm
|
||||
41
Source/Params.h
Normal file
41
Source/Params.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
|
||||
namespace prm {
|
||||
|
||||
enum class Id : int {
|
||||
osc1Wave, osc2Wave, detune, oscMix,
|
||||
fltType, fltCutoff, fltRes, fltEnv, fltKey,
|
||||
ampA, ampD, ampS, ampR,
|
||||
fenvA, fenvD, fenvS, fenvR,
|
||||
lfoRate, lfoShape, lfoDepth, lfoTarget,
|
||||
fxType, fxAmt, fxTime,
|
||||
vol, pan, poly, glide, tune
|
||||
};
|
||||
|
||||
constexpr int num = 29;
|
||||
|
||||
struct Desc {
|
||||
const char* id;
|
||||
const char* name;
|
||||
const char* shortName;
|
||||
float min, max, def;
|
||||
bool discrete;
|
||||
float fine, coarse;
|
||||
};
|
||||
|
||||
const Desc& desc (Id id);
|
||||
bool validId (int i);
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout createLayout();
|
||||
|
||||
juce::String formatParam (Id id, float v);
|
||||
float changeParam (Id id, float cur, int dir, bool coarse);
|
||||
|
||||
const char* waveName (int w);
|
||||
const char* fltTypeName (int t);
|
||||
const char* lfoShapeName (int s);
|
||||
const char* lfoTargetName (int t);
|
||||
const char* fxTypeName (int t);
|
||||
|
||||
} // namespace prm
|
||||
196
Source/PluginEditor.cpp
Normal file
196
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include "PluginEditor.h"
|
||||
#include "BuildInfo.h"
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
|
||||
RackEditor::RackEditor (RMX19AudioProcessor& p)
|
||||
: AudioProcessorEditor (p)
|
||||
, proc (p)
|
||||
, display (p)
|
||||
{
|
||||
face = juce::Colour (0xff16130f);
|
||||
|
||||
up = new NavButton (-1, "^", "UP");
|
||||
down = new NavButton (1, "v", "DOWN");
|
||||
left = new NavButton (-1, "<", "LEFT");
|
||||
right = new NavButton (1, ">", "RIGHT");
|
||||
enterB = new OkButton ("ENTER", true);
|
||||
backB = new OkButton ("BACK", false);
|
||||
|
||||
for (auto* b : { (juce::Button*) up, (juce::Button*) down,
|
||||
(juce::Button*) left, (juce::Button*) right,
|
||||
(juce::Button*) enterB, (juce::Button*) backB })
|
||||
addAndMakeVisible (b);
|
||||
|
||||
up->onClick = [this] { display.navUp(); };
|
||||
down->onClick = [this] { display.navDown(); };
|
||||
left->onClick = [this] { display.navLeft(); };
|
||||
right->onClick = [this] { display.navRight(); };
|
||||
enterB->onClick = [this] { display.navEnter(); };
|
||||
backB->onClick = [this] { display.navExit(); };
|
||||
|
||||
addAndMakeVisible (display);
|
||||
|
||||
setWantsKeyboardFocus (true);
|
||||
setSize (656, 248);
|
||||
startTimerHz (18);
|
||||
}
|
||||
|
||||
RackEditor::~RackEditor()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void RackEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
const auto b = getLocalBounds();
|
||||
|
||||
// --- metal rack face ---
|
||||
g.setColour (face);
|
||||
g.fillRect (b);
|
||||
|
||||
// brushed texture
|
||||
g.setColour (juce::Colour::fromRGBA (255, 255, 255, 7));
|
||||
for (int x = 0; x < b.getWidth(); x += 3)
|
||||
g.fillRect (x, 0, 1, b.getHeight());
|
||||
|
||||
// rack rails (top/bottom)
|
||||
g.setColour (juce::Colour (0xff221e1a));
|
||||
g.fillRect (0, 0, b.getWidth(), 4);
|
||||
g.fillRect (0, b.getHeight() - 4, b.getWidth(), 4);
|
||||
g.setColour (juce::Colour (0xff5a5347));
|
||||
g.fillRect (0, 4, b.getWidth(), 1);
|
||||
g.fillRect (0, b.getHeight() - 5, b.getWidth(), 1);
|
||||
|
||||
// screw holes
|
||||
const auto screw = [&] (int x, int y)
|
||||
{
|
||||
g.setColour (juce::Colour (0xff2a2620));
|
||||
g.fillEllipse (x - 4, y - 4, 8, 8);
|
||||
g.setColour (juce::Colour (0xff8a7f6e));
|
||||
g.drawEllipse (x - 4.5f, y - 4.5f, 9, 9, 1.0f);
|
||||
g.drawLine (x - 3, y, x + 3, y, 1.0f);
|
||||
};
|
||||
screw (8, 8); screw (b.getWidth() - 9, 8);
|
||||
screw (8, b.getHeight() - 9); screw (b.getWidth() - 9, b.getHeight() - 9);
|
||||
|
||||
// brand plate
|
||||
g.setColour (juce::Colour (0xff614f3a));
|
||||
g.fillRect (12, 6, 150, 12);
|
||||
g.setColour (juce::Colour (0xffc9a86f));
|
||||
g.setFont (juce::FontOptions ("Arial", 10.0f, juce::Font::bold));
|
||||
g.drawFittedText ("RMX-19 RACK SYNTH", 12, 6, 150, 12, juce::Justification::centred, 1);
|
||||
|
||||
// power LED (right of LCD status)
|
||||
const int ledX = getWidth() - 20, ledY = 18;
|
||||
g.setColour (juce::Colour (0xff3a0d0d));
|
||||
g.fillEllipse (ledX - 3, ledY - 3, 8, 8);
|
||||
g.setColour (juce::Colour::fromFloatRGBA (0.95f, 0.25f + 0.1f * powerPulse, 0.1f, 1.0f));
|
||||
g.fillEllipse (ledX - 2, ledY - 2, 6, 6);
|
||||
g.setColour (juce::Colour (0xff8a7f6e));
|
||||
g.setFont (juce::FontOptions ("Arial", 8.0f, juce::Font::plain));
|
||||
g.drawFittedText ("PWR", ledX - 26, ledY - 5, 20, 10, juce::Justification::centredRight, 1);
|
||||
|
||||
// rear engraving text
|
||||
g.setColour (juce::Colour (0xff6e6657));
|
||||
g.setFont (juce::FontOptions ("Courier New", 8.0f, juce::Font::plain));
|
||||
const juce::String ser = "S/N 0001-1943 MIDI OUT (R) MIDI IN (R) AUDIO 24BIT/96K";
|
||||
g.drawFittedText (ser, 12, getHeight() - 16, getWidth() - 24, 11, juce::Justification::centred, 1);
|
||||
}
|
||||
|
||||
void RackEditor::resized()
|
||||
{
|
||||
const auto b = getLocalBounds();
|
||||
|
||||
// LCD recessed module
|
||||
const juce::Rectangle<int> lcdArea (14, 24, 356, 168);
|
||||
display.setBounds (lcdArea);
|
||||
|
||||
// nav diamond + enter/back to the right of the LCD
|
||||
const juce::Rectangle<int> navArea (lcdArea.getRight() + 16, 26, b.getWidth() - lcdArea.getRight() - 30, 164);
|
||||
|
||||
const int bw = 58, bh = 20;
|
||||
const int cx = navArea.getX() + (navArea.getWidth() - bw) / 2;
|
||||
const int rowY = navArea.getY();
|
||||
|
||||
up->setBounds (cx, rowY, bw, bh);
|
||||
left->setBounds (navArea.getX() + 2, rowY + bh + 6, bw, bh);
|
||||
down->setBounds (cx, rowY + bh + 6, bw, bh);
|
||||
right->setBounds (navArea.getRight() - bw - 2, rowY + bh + 6, bw, bh);
|
||||
|
||||
enterB->setBounds (navArea.getX() + 2, rowY + 2 * (bh + 6) + 4, bw, bh);
|
||||
backB->setBounds (navArea.getRight() - bw - 2, rowY + 2 * (bh + 6) + 4, bw, bh);
|
||||
}
|
||||
|
||||
void RackEditor::timerCallback()
|
||||
{
|
||||
powerPulse = 0.5f + 0.5f * std::sin (juce::Time::getMillisecondCounter() * 0.0022f);
|
||||
repaint (getLocalBounds());
|
||||
}
|
||||
|
||||
bool RackEditor::keyPressed (const juce::KeyPress& k)
|
||||
{
|
||||
if (display.keyTrap (k))
|
||||
{
|
||||
repaint();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- buttons -------------------------------------------------------------
|
||||
|
||||
void RackEditor::NavButton::paintButton (juce::Graphics& g, bool, bool down)
|
||||
{
|
||||
const auto b = getLocalBounds();
|
||||
const juce::Colour base = juce::Colour (0xff221d18);
|
||||
const juce::Colour edge = juce::Colour (0xff6b5f4e);
|
||||
const juce::Colour text = juce::Colour (0xfffff3d8);
|
||||
|
||||
if (down)
|
||||
{
|
||||
g.setColour (edge.darker (0.4f));
|
||||
g.fillRect (b);
|
||||
g.setColour (base.brighter (0.1f));
|
||||
g.fillRect (b.reduced (2, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour (base);
|
||||
g.fillRect (b);
|
||||
g.setColour (edge);
|
||||
g.drawRect (b, 1);
|
||||
}
|
||||
|
||||
// glyph
|
||||
g.setColour (text);
|
||||
g.setFont (juce::FontOptions ("Arial", 12.0f, juce::Font::bold));
|
||||
g.drawFittedText (mark, b.withTrimmedBottom (9), juce::Justification::centred, 1);
|
||||
g.setFont (juce::FontOptions ("Arial", 7.0f, juce::Font::plain));
|
||||
g.drawFittedText (getName(), b.withTrimmedTop (b.getHeight() - 8), juce::Justification::centred, 1);
|
||||
}
|
||||
|
||||
void RackEditor::OkButton::paintButton (juce::Graphics& g, bool over, bool down)
|
||||
{
|
||||
const auto b = getLocalBounds();
|
||||
const juce::Colour base = enter ? juce::Colour (0xff263a28) : juce::Colour (0xff332a20);
|
||||
const juce::Colour edge = juce::Colour (0xff8a9b6a);
|
||||
|
||||
if (down)
|
||||
{
|
||||
g.setColour (edge.darker (0.35f));
|
||||
g.fillRect (b);
|
||||
g.setColour (base.brighter (0.12f));
|
||||
g.fillRect (b.reduced (2, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour (base);
|
||||
g.fillRect (b);
|
||||
g.setColour (edge);
|
||||
g.drawRect (b, 1);
|
||||
}
|
||||
|
||||
g.setColour (over ? juce::Colour (0xffffd977) : juce::Colour (0xffd8d2c6));
|
||||
g.setFont (juce::FontOptions ("Arial", 9.0f, juce::Font::bold));
|
||||
g.drawFittedText (getName(), b, juce::Justification::centred, 1);
|
||||
}
|
||||
53
Source/PluginEditor.h
Normal file
53
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
#include "LcdPanel.h"
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
class RackEditor : public juce::AudioProcessorEditor, private juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit RackEditor (RMX19AudioProcessor&);
|
||||
~RackEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
bool keyPressed (const juce::KeyPress&) override;
|
||||
|
||||
private:
|
||||
RMX19AudioProcessor& proc;
|
||||
|
||||
class NavButton : public juce::Button
|
||||
{
|
||||
public:
|
||||
NavButton (int d, const juce::String& glyph, const juce::String& label)
|
||||
: juce::Button (label), dir (d), mark (glyph)
|
||||
{ setWantsKeyboardFocus (true); setRepeatSpeed (320, 80); }
|
||||
void paintButton (juce::Graphics&, bool, bool) override;
|
||||
int dir;
|
||||
juce::String mark;
|
||||
};
|
||||
|
||||
class OkButton : public juce::Button
|
||||
{
|
||||
public:
|
||||
OkButton (const juce::String& label, bool isEnter)
|
||||
: juce::Button (label), enter (isEnter)
|
||||
{ setWantsKeyboardFocus (true); setRepeatSpeed (320, 80); }
|
||||
void paintButton (juce::Graphics&, bool, bool) override;
|
||||
bool enter;
|
||||
};
|
||||
|
||||
NavButton* up = nullptr; NavButton* down = nullptr;
|
||||
NavButton* left = nullptr; NavButton* right = nullptr;
|
||||
OkButton* enterB = nullptr; OkButton* backB = nullptr;
|
||||
|
||||
LcdPanel display;
|
||||
|
||||
juce::Colour face;
|
||||
float powerPulse = 0.0f;
|
||||
|
||||
void timerCallback() override;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RackEditor)
|
||||
};
|
||||
264
Source/PluginProcessor.cpp
Normal file
264
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class RMX19AudioProcessor::FxEngine
|
||||
{
|
||||
public:
|
||||
void prepare (double sampleRate, int maxSamples)
|
||||
{
|
||||
sr = sampleRate;
|
||||
chorusL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
chorusR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
delL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
delR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
|
||||
const int maxDelay = (int) (sampleRate * 1.5);
|
||||
chorusL.setMaximumDelayInSamples (maxDelay);
|
||||
chorusR.setMaximumDelayInSamples (maxDelay);
|
||||
delL.setMaximumDelayInSamples (maxDelay);
|
||||
delR.setMaximumDelayInSamples (maxDelay);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
chorusL.reset(); chorusR.reset();
|
||||
delL.reset(); delR.reset();
|
||||
chPh = 0.0;
|
||||
}
|
||||
|
||||
void process (const VParams& p, float* L, float* R, int num)
|
||||
{
|
||||
const int type = (int) p.fxType;
|
||||
if (type == 0) return;
|
||||
|
||||
const float wet = p.fxAmt;
|
||||
const float dry = 1.0f - wet * 0.5f;
|
||||
const float chBase = 0.008f * (float) sr; // 8 ms chorus base
|
||||
const float chDepth = 0.0035f * (float) sr; // +-3.5 ms wobble
|
||||
const float delSamples = (float) (p.fxTime * sr);
|
||||
const float feedback = 0.40f;
|
||||
|
||||
for (int s = 0; s < num; ++s)
|
||||
{
|
||||
const float inL = L[s], inR = R[s];
|
||||
float outL = inL * dry, outR = inR * dry;
|
||||
|
||||
if (type == 1 || type == 3)
|
||||
{
|
||||
const double rate = type == 3 ? 0.55 : 0.38;
|
||||
chPh += rate / sr;
|
||||
if (chPh >= 1.0) chPh -= 1.0;
|
||||
const float m = 0.5f + 0.5f * (float) std::sin (chPh * 2.0 * juce::MathConstants<double>::pi);
|
||||
|
||||
chorusL.pushSample (0, inL);
|
||||
chorusR.pushSample (0, inR);
|
||||
const float mL = chorusL.popSample (0, chBase + chDepth * m);
|
||||
const float mR = chorusR.popSample (0, chBase + chDepth * (1.0f - m));
|
||||
outL += mL * wet * 0.7f;
|
||||
outR += mR * wet * 0.7f;
|
||||
}
|
||||
|
||||
if (type == 2 || type == 3)
|
||||
{
|
||||
const float rL = delL.popSample (0, delSamples);
|
||||
const float rR = delR.popSample (0, delSamples);
|
||||
delL.pushSample (0, inL + rR * feedback);
|
||||
delR.pushSample (0, inR + rL * feedback);
|
||||
outL += rL * wet;
|
||||
outR += rR * wet;
|
||||
}
|
||||
|
||||
L[s] = outL;
|
||||
R[s] = outR;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
juce::dsp::DelayLine<float> chorusL, chorusR, delL, delR;
|
||||
double sr = 48000.0;
|
||||
double chPh = 0.0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
RMX19AudioProcessor::RMX19AudioProcessor()
|
||||
: AudioProcessor (BusesProperties().withOutput ("Output", juce::AudioChannelSet::stereo(), true))
|
||||
, apvts (*this, nullptr, "Params", createParams())
|
||||
{
|
||||
}
|
||||
|
||||
RMX19AudioProcessor::~RMX19AudioProcessor() {}
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout RMX19AudioProcessor::createParams()
|
||||
{
|
||||
return prm::createLayout();
|
||||
}
|
||||
|
||||
const juce::String RMX19AudioProcessor::getName() const { return "RMX-19"; }
|
||||
|
||||
void RMX19AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
engine.setSampleRate (sampleRate);
|
||||
engine.allNotesOff (true);
|
||||
fx = std::make_unique<FxEngine>();
|
||||
fx->prepare (sampleRate, samplesPerBlock);
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::releaseResources()
|
||||
{
|
||||
engine.allNotesOff (true);
|
||||
if (fx) fx->reset();
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
|
||||
{
|
||||
(void) p;
|
||||
constexpr double bendSemis = 2.0;
|
||||
|
||||
for (const auto meta : midi)
|
||||
{
|
||||
const auto msg = meta.getMessage();
|
||||
lastMidi = juce::Time::getMillisecondCounter();
|
||||
|
||||
if (msg.isNoteOn())
|
||||
{
|
||||
engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
||||
}
|
||||
else if (msg.isNoteOff())
|
||||
{
|
||||
engine.noteOff (msg.getNoteNumber());
|
||||
}
|
||||
else if (msg.isPitchWheel())
|
||||
{
|
||||
engine.pitchBend ((float) ((msg.getPitchWheelValue() - 8192) / 8192.0 * bendSemis * 100.0));
|
||||
}
|
||||
else if (msg.isController())
|
||||
{
|
||||
const int cc = msg.getControllerNumber();
|
||||
const float v = msg.getControllerValue() / 127.0f;
|
||||
switch (cc)
|
||||
{
|
||||
case 64: engine.sustainPedal (msg.getControllerValue() >= 64); break;
|
||||
case 1: engine.modWheel (v); break;
|
||||
case 7: if (auto* par = apvts.getParameter ("vol")) par->setValueNotifyingHost (v); break;
|
||||
case 10: if (auto* par = apvts.getParameter ("pan")) par->setValueNotifyingHost (v * 2.0f - 1.0f); break;
|
||||
case 71: if (auto* par = apvts.getParameter ("fltres")) par->setValueNotifyingHost (v); break;
|
||||
case 72: if (auto* par = apvts.getParameter ("ampR")) par->setValueNotifyingHost (v); break;
|
||||
case 73: if (auto* par = apvts.getParameter ("ampA")) par->setValueNotifyingHost (v); break;
|
||||
case 74: if (auto* par = apvts.getParameter ("fltcut")) par->setValueNotifyingHost (v); break;
|
||||
case 76: if (auto* par = apvts.getParameter ("lforate")) par->setValueNotifyingHost (v); break;
|
||||
case 77: if (auto* par = apvts.getParameter ("lfodepth"))par->setValueNotifyingHost (v); break;
|
||||
case 91: if (auto* par = apvts.getParameter ("fxamt")) par->setValueNotifyingHost (v); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
const juce::uint32 start = juce::Time::getMillisecondCounter();
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const int numChannels = buffer.getNumChannels();
|
||||
if (numSamples == 0) return;
|
||||
|
||||
VParams p;
|
||||
p.update (apvts);
|
||||
|
||||
// patch follow
|
||||
const int sel = (int) apvts.getRawParameterValue ("patchsel")->load();
|
||||
if (sel != lastPatchParam && sel >= 0 && sel < bank.size())
|
||||
{
|
||||
lastPatchParam = sel;
|
||||
patchIndex = sel;
|
||||
bank.writeTo (sel, apvts);
|
||||
}
|
||||
|
||||
processMidi (midi, p);
|
||||
|
||||
const int chL = 0, chR = 1;
|
||||
float* L = buffer.getNumChannels() > 0 ? buffer.getWritePointer (juce::jmin (chL, numChannels - 1)) : nullptr;
|
||||
float* R = buffer.getNumChannels() > 1 ? buffer.getWritePointer (juce::jmin (chR, numChannels - 1)) : nullptr;
|
||||
|
||||
for (int c = 0; c < numChannels; ++c)
|
||||
buffer.clear (c, 0, numSamples);
|
||||
|
||||
if (L != nullptr && R != nullptr)
|
||||
{
|
||||
engine.render (p, L, R, numSamples);
|
||||
if (fx) fx->process (p, L, R, numSamples);
|
||||
if (p.vol != 1.0f) buffer.applyGain (p.vol);
|
||||
}
|
||||
else if (L != nullptr)
|
||||
{
|
||||
juce::AudioBuffer<float> dummy (1, numSamples);
|
||||
dummy.clear();
|
||||
float* dL = dummy.getWritePointer (0);
|
||||
float* dR = dummy.getWritePointer (0);
|
||||
engine.render (p, dL, dR, numSamples);
|
||||
for (int c = 0; c < numChannels; ++c)
|
||||
buffer.copyFrom (c, 0, dummy, 0, 0, numSamples);
|
||||
buffer.applyGain (p.vol);
|
||||
}
|
||||
|
||||
cpu = 0.7f * cpu + 0.3f * (float) (juce::Time::getMillisecondCounter() - start);
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::selectPatch (int i)
|
||||
{
|
||||
i = juce::jlimit (0, bank.size() - 1, i);
|
||||
patchIndex = i;
|
||||
lastPatchParam = i;
|
||||
if (auto* par = apvts.getParameter ("patchsel"))
|
||||
par->setValueNotifyingHost (par->convertTo0to1 ((float) i));
|
||||
bank.writeTo (i, apvts);
|
||||
}
|
||||
|
||||
int RMX19AudioProcessor::getNumPrograms() { return bank.size(); }
|
||||
int RMX19AudioProcessor::getCurrentProgram() { return patchIndex; }
|
||||
|
||||
void RMX19AudioProcessor::setCurrentProgram (int index) { selectPatch (index); }
|
||||
|
||||
const juce::String RMX19AudioProcessor::getProgramName (int index)
|
||||
{
|
||||
if (index >= 0 && index < bank.size()) return bank.get (index).name;
|
||||
return {};
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::changeProgramName (int, const juce::String&) {}
|
||||
|
||||
void RMX19AudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
||||
copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||
if (xml == nullptr) return;
|
||||
|
||||
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
||||
|
||||
const int sel = juce::jlimit (0, bank.size() - 1,
|
||||
(int) apvts.getRawParameterValue ("patchsel")->load());
|
||||
lastPatchParam = sel;
|
||||
patchIndex = sel;
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* RMX19AudioProcessor::createEditor()
|
||||
{
|
||||
return new RackEditor (*this);
|
||||
}
|
||||
|
||||
bool RMX19AudioProcessor::hasEditor() const { return true; }
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new RMX19AudioProcessor();
|
||||
}
|
||||
64
Source/PluginProcessor.h
Normal file
64
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "Params.h"
|
||||
#include "PresetBank.h"
|
||||
#include "SynthEngine.h"
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
|
||||
class RMX19AudioProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
RMX19AudioProcessor();
|
||||
~RMX19AudioProcessor() 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;
|
||||
|
||||
const juce::String getName() const override;
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 3.0; }
|
||||
|
||||
int getNumPrograms() override;
|
||||
int getCurrentProgram() override;
|
||||
void setCurrentProgram (int) override;
|
||||
const juce::String getProgramName (int) override;
|
||||
void changeProgramName (int, const juce::String&) override;
|
||||
|
||||
void getStateInformation (juce::MemoryBlock&) override;
|
||||
void setStateInformation (const void*, int) override;
|
||||
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
|
||||
PresetBank& getBank() { return bank; }
|
||||
SynthEngine& getEngine() { return engine; }
|
||||
int getPatchIndex() const noexcept { return patchIndex; }
|
||||
void selectPatch (int i);
|
||||
|
||||
void getMeters (float& l, float& r) const noexcept { engine.getMeters (l, r); }
|
||||
int getActiveVoices() const noexcept { return engine.activeVoices(); }
|
||||
juce::uint32 getLastMidiTime() const noexcept { return lastMidi; }
|
||||
float getCpu() const noexcept { return cpu; }
|
||||
|
||||
private:
|
||||
void processMidi (juce::MidiBuffer&, const VParams&);
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout createParams();
|
||||
|
||||
class FxEngine;
|
||||
std::unique_ptr<FxEngine> fx;
|
||||
|
||||
PresetBank bank;
|
||||
SynthEngine engine;
|
||||
int patchIndex = 0;
|
||||
int lastPatchParam = 0;
|
||||
juce::uint32 lastMidi = 0;
|
||||
float cpu = 0.0f;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RMX19AudioProcessor)
|
||||
};
|
||||
151
Source/PresetBank.cpp
Normal file
151
Source/PresetBank.cpp
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#include "PresetBank.h"
|
||||
|
||||
// Oscillator wave indices (see prm::waveName)
|
||||
enum { O_SIN = 0, O_TRI, O_SAW, O_PUL, O_NOI };
|
||||
|
||||
// Parameter order matches the prm::desc table:
|
||||
// 0 osc1Wave 1 osc2Wave 2 detune 3 oscMix
|
||||
// 4 fltType 5 fltCutoff 6 fltRes 7 fltEnv 8 fltKey
|
||||
// 9 ampA 10 ampD 11 ampS 12 ampR
|
||||
// 13 fenvA 14 fenvD 15 fenvS 16 fenvR
|
||||
// 17 lfoRate 18 lfoShape 19 lfoDepth 20 lfoTarget
|
||||
// 21 fxType 22 fxAmt 23 fxTime
|
||||
// 24 vol 25 pan 26 poly 27 glide 28 tune
|
||||
|
||||
static const Patch defs[] = {
|
||||
// 1 Warm Pad ---------------------------------------------------------
|
||||
{ "Warm Pad", "POLY PAD", { O_SAW, O_SAW, 8, 55, 1, 2600, 0.20f, 60, 35,
|
||||
0.60f, 1.6f, 0.80f, 1.20f,
|
||||
0.40f, 0.9f, 0.35f, 0.5f,
|
||||
4.0f, 0, 0.18f, 0,
|
||||
1, 0.32f, 0.30f,
|
||||
0.82f, 0.0f, 12, 0.10f, 0 } },
|
||||
// 2 Glass Pad --------------------------------------------------------
|
||||
{ "Glass Pad", "POLY PAD", { O_TRI, O_TRI, 12, 60, 2, 1400, 0.55f, 75, 30,
|
||||
0.80f, 1.8f, 0.90f, 1.60f,
|
||||
0.50f, 1.2f, 0.40f, 0.8f,
|
||||
5.2f, 0, 0.16f, 2,
|
||||
3, 0.38f, 0.40f,
|
||||
0.80f, 0.0f, 12, 0.10f, 0 } },
|
||||
// 3 Dream Pad --------------------------------------------------------
|
||||
{ "Dream Pad", "POLY PAD", { O_SAW, O_PUL, -7, 50, 1, 3000, 0.25f, 70, 40,
|
||||
1.10f, 2.4f, 0.85f, 1.80f,
|
||||
0.60f, 1.4f, 0.30f, 0.9f,
|
||||
0.35f, 1, 0.20f, 0,
|
||||
1, 0.40f, 0.50f,
|
||||
0.80f, 0.0f, 12, 0.15f, 0 } },
|
||||
// 4 Movie Strings ----------------------------------------------------
|
||||
{ "Movie Strings", "POLY PAD", { O_SAW, O_SAW, 6, 45, 1, 2200, 0.15f, 65, 55,
|
||||
0.50f, 1.2f, 0.80f, 0.70f,
|
||||
0.40f, 0.8f, 0.35f, 0.5f,
|
||||
3.1f, 0, 0.10f, 0,
|
||||
3, 0.28f, 0.45f,
|
||||
0.82f, 0.0f, 12, 0.08f, 0 } },
|
||||
// 5 Electric Piano ---------------------------------------------------
|
||||
{ "Electric Piano", "KEY", { O_PUL, O_TRI, 5, 62, 1, 2800, 0.25f, 45, 40,
|
||||
0.004f, 1.4f, 0.30f, 0.40f,
|
||||
0.004f, 0.2f, 0.10f, 0.3f,
|
||||
6.0f, 0, 0.05f, 2,
|
||||
1, 0.35f, 0.30f,
|
||||
0.80f, 0.0f, 12, 0.0f, 0 } },
|
||||
// 6 Acoustic Piano ---------------------------------------------------
|
||||
{ "Acoustic Piano", "KEY", { O_SAW, O_TRI, 3, 55, 1, 5200, 0.18f, 30, 60,
|
||||
0.003f, 2.2f, 0.25f, 0.50f,
|
||||
0.002f, 0.25f, 0.05f, 0.35f,
|
||||
4.0f, 0, 0.03f, 2,
|
||||
1, 0.22f, 0.30f,
|
||||
0.80f, 0.0f, 12, 0.0f, 0 } },
|
||||
// 7 Toy Piano --------------------------------------------------------
|
||||
{ "Toy Piano", "KEY", { O_PUL, O_PUL, 0, 70, 2, 2100, 0.50f, 30, 45,
|
||||
0.002f, 0.35f, 0.0f, 0.18f,
|
||||
0.002f, 0.05f, 0.02f, 0.08f,
|
||||
5.0f, 0, 0.03f, 2,
|
||||
2, 0.18f, 0.17f,
|
||||
0.78f, 0.0f, 12, 0.0f, 0 } },
|
||||
// 8 Clavinet 70 ------------------------------------------------------
|
||||
{ "Clavinet 70", "KEY", { O_PUL, O_PUL, 0, 40, 2, 1500, 0.40f, 60, 30,
|
||||
0.002f, 0.50f, 0.45f, 0.12f,
|
||||
0.001f, 0.05f, 0.05f, 0.10f,
|
||||
4.0f, 0, 0.05f, 0,
|
||||
1, 0.30f, 0.28f,
|
||||
0.84f, 0.0f, 12, 0.0f, 0 } },
|
||||
// 9 Organ Drawbar ----------------------------------------------------
|
||||
{ "Organ Drawbar", "KEY", { O_SAW, O_SAW, 0, 50, 1, 10000, 0.10f, 20, 80,
|
||||
0.010f, 0.10f, 0.98f, 0.10f,
|
||||
0.010f, 0.10f, 0.50f, 0.10f,
|
||||
8.0f, 3, 0.03f, 1,
|
||||
1, 0.20f, 0.28f,
|
||||
0.82f, 0.0f, 8, 0.0f, 0 } },
|
||||
// 10 FM E.Piano ------------------------------------------------------
|
||||
{ "FM E.Piano", "KEY", { O_SIN, O_TRI, 0, 30, 1, 4000, 0.12f, 50, 50,
|
||||
0.002f, 1.0f, 0.50f, 0.30f,
|
||||
0.002f, 0.20f, 0.10f, 0.25f,
|
||||
4.5f, 0, 0.04f, 2,
|
||||
1, 0.40f, 0.30f,
|
||||
0.78f, 0.0f, 12, 0.0f, 0 } },
|
||||
// 11 Analog Bass -----------------------------------------------------
|
||||
{ "Analog Bass", "BASS", { O_PUL, O_SIN, 0, 80, 1, 700, 0.35f, 35, 10,
|
||||
0.002f, 0.30f, 0.82f, 0.12f,
|
||||
0.002f, 0.10f, 0.15f, 0.10f,
|
||||
3.0f, 0, 0.05f, 0,
|
||||
0, 0.0f, 0.30f,
|
||||
0.90f, 0.0f, 1, 0.04f, 0 } },
|
||||
// 12 Sub Rez Bass ----------------------------------------------------
|
||||
{ "Sub Rez Bass", "BASS", { O_SAW, O_PUL, -4, 45, 1, 520, 0.70f, 55, 15,
|
||||
0.004f, 0.40f, 0.70f, 0.10f,
|
||||
0.004f, 0.15f, 0.25f, 0.10f,
|
||||
2.4f, 3, 0.30f, 0,
|
||||
0, 0.0f, 0.30f,
|
||||
0.90f, 0.0f, 2, 0.03f, 0 } },
|
||||
// 13 Saw Lead --------------------------------------------------------
|
||||
{ "Saw Lead", "LEAD", { O_SAW, O_SAW, 14, 50, 1, 3800, 0.25f, 30, 60,
|
||||
0.005f, 0.30f, 0.80f, 0.20f,
|
||||
0.005f, 0.15f, 0.30f, 0.15f,
|
||||
5.4f, 0, 0.14f, 1,
|
||||
0, 0.0f, 0.28f,
|
||||
0.86f, 0.0f, 4, 0.06f, 0 } },
|
||||
// 14 Mr. Square ------------------------------------------------------
|
||||
{ "Mr. Square", "LEAD", { O_PUL, O_PUL, 7, 60, 1, 2400, 0.22f, 20, 70,
|
||||
0.003f, 0.25f, 0.72f, 0.18f,
|
||||
0.003f, 0.12f, 0.25f, 0.12f,
|
||||
6.6f, 0, 0.10f, 1,
|
||||
0, 0.0f, 0.25f,
|
||||
0.84f, 0.0f, 4, 0.05f, 0 } },
|
||||
// 15 Choir Layered ---------------------------------------------------
|
||||
{ "Choir Layered", "POLY PAD",{ O_SAW, O_PUL, 9, 48, 1, 2600, 0.14f, 62, 45,
|
||||
0.35f, 0.80f, 0.85f, 1.10f,
|
||||
0.30f, 0.60f, 0.35f, 0.60f,
|
||||
3.4f, 1, 0.12f, 0,
|
||||
1, 0.38f, 0.42f,
|
||||
0.82f, 0.0f, 12, 0.08f, 0 } },
|
||||
// 16 8-bit Chiptune --------------------------------------------------
|
||||
{ "8bit Chiptune", "LEAD", { O_PUL, O_PUL, 0, 30, 1, 9000, 0.10f, 10, 70,
|
||||
0.003f, 0.12f, 0.90f, 0.12f,
|
||||
0.003f, 0.06f, 0.10f, 0.08f,
|
||||
9.0f, 0, 0.05f, 0,
|
||||
0, 0.0f, 0.20f,
|
||||
0.80f, 0.0f, 6, 0.0f, 0 } },
|
||||
};
|
||||
|
||||
PresetBank::PresetBank()
|
||||
{
|
||||
patches.assign (defs, defs + juce::numElementsInArray (defs));
|
||||
}
|
||||
|
||||
int PresetBank::findByName (const juce::String& name) const
|
||||
{
|
||||
for (int i = 0; i < patches.size(); ++i)
|
||||
if (juce::String (patches[i].name) == name)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void PresetBank::writeTo (int i, juce::AudioProcessorValueTreeState& apvts) const
|
||||
{
|
||||
const int n = (int) patches.size();
|
||||
i = juce::jlimit (0, n - 1, i);
|
||||
const Patch& p = patches[i];
|
||||
for (int k = 0; k < prm::num; ++k)
|
||||
if (auto* par = apvts.getParameter (prm::desc (static_cast<prm::Id> (k)).id))
|
||||
par->setValueNotifyingHost (par->convertTo0to1 (p.v[k]));
|
||||
}
|
||||
26
Source/PresetBank.h
Normal file
26
Source/PresetBank.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
#include "Params.h"
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <vector>
|
||||
|
||||
struct Patch
|
||||
{
|
||||
const char* name;
|
||||
const char* category;
|
||||
float v[prm::num];
|
||||
};
|
||||
|
||||
class PresetBank
|
||||
{
|
||||
public:
|
||||
PresetBank();
|
||||
|
||||
int size() const noexcept { return (int) patches.size(); }
|
||||
const Patch& get (int i) const { return patches[(size_t) i]; }
|
||||
int findByName (const juce::String&) const;
|
||||
|
||||
void writeTo (int i, juce::AudioProcessorValueTreeState&) const;
|
||||
|
||||
private:
|
||||
std::vector<Patch> patches;
|
||||
};
|
||||
235
Source/SynthEngine.cpp
Normal file
235
Source/SynthEngine.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
#include "SynthEngine.h"
|
||||
|
||||
SynthEngine::SynthEngine() { rng.setSeedRandomly(); }
|
||||
SynthEngine::~SynthEngine() {}
|
||||
|
||||
void SynthEngine::setSampleRate (double s)
|
||||
{
|
||||
sr = s;
|
||||
}
|
||||
|
||||
float SynthEngine::osc (int wave, double ph)
|
||||
{
|
||||
switch (wave)
|
||||
{
|
||||
case 0: return (float) std::sin (ph * 2.0 * juce::MathConstants<double>::pi);
|
||||
case 1: return (float) (ph < 0.5 ? 4.0 * ph - 1.0 : 3.0 - 4.0 * ph);
|
||||
case 2: return (float) (1.0 - 2.0 * ph);
|
||||
case 3: return (float) (ph < 0.5 ? -1.0 : 1.0);
|
||||
case 4: return rng.nextFloat() * 2.0f - 1.0f;
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void SynthEngine::allNotesOff (bool immediate)
|
||||
{
|
||||
for (auto& v : voices)
|
||||
{
|
||||
if (v.active)
|
||||
{
|
||||
if (immediate) v.active = false;
|
||||
else v.amp.noteOff();
|
||||
}
|
||||
}
|
||||
sustainedNotes.clear();
|
||||
if (immediate) active = 0;
|
||||
}
|
||||
|
||||
void SynthEngine::sustainPedal (bool down)
|
||||
{
|
||||
pedal = down;
|
||||
if (! down)
|
||||
{
|
||||
for (auto n : sustainedNotes)
|
||||
for (auto& v : voices)
|
||||
if (v.active && v.note == n)
|
||||
v.amp.noteOff();
|
||||
sustainedNotes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void SynthEngine::pitchBend (float cents) { bendCents = cents; }
|
||||
void SynthEngine::modWheel (float v) { mod = juce::jlimit (0.0f, 1.0f, v); }
|
||||
|
||||
SynthEngine::Voice* SynthEngine::acquireVoice()
|
||||
{
|
||||
const int lim = juce::jlimit (1, maxVoice, polyLimit);
|
||||
for (int i = 0; i < lim; ++i)
|
||||
if (! voices[i].active)
|
||||
return &voices[i];
|
||||
|
||||
stealIdx = (stealIdx + 1) % lim;
|
||||
return &voices[stealIdx];
|
||||
}
|
||||
|
||||
void SynthEngine::noteOn (int midiNote, float velocity)
|
||||
{
|
||||
const float noteFreq = (float) (440.0 * std::pow (2.0, (midiNote - 69.0) / 12.0));
|
||||
Voice* v = acquireVoice();
|
||||
v->active = true;
|
||||
v->note = midiNote;
|
||||
v->vel = 1.0f - std::pow (1.0f - velocity, 1.35f);
|
||||
v->curFreq = (lastGlide > 0.001f && lastNoteFreq > 0.0f) ? lastNoteFreq : noteFreq;
|
||||
v->targetFreq = noteFreq;
|
||||
v->ph1 = rng.nextDouble() * 0.5;
|
||||
v->ph2 = rng.nextDouble() * 0.5;
|
||||
v->lfoPh = rng.nextDouble();
|
||||
v->lfoIdx = 0;
|
||||
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
|
||||
v->lp = v->bp = 0.0f;
|
||||
v->outSmooth = 0.0f;
|
||||
|
||||
const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f);
|
||||
const float ang = (p + 1.0f) * juce::MathConstants<float>::pi * 0.25f;
|
||||
const float mag = 0.7071f + 0.25f * rng.nextFloat();
|
||||
v->panL = std::cos (ang) * mag;
|
||||
v->panR = std::sin (ang) * mag;
|
||||
|
||||
v->amp.reset(); v->flt.reset();
|
||||
v->amp.noteOn(); v->flt.noteOn();
|
||||
lastNoteFreq = noteFreq;
|
||||
++active;
|
||||
}
|
||||
|
||||
void SynthEngine::noteOff (int midiNote)
|
||||
{
|
||||
if (pedal)
|
||||
{
|
||||
if (! sustainedNotes.contains (midiNote))
|
||||
sustainedNotes.add (midiNote);
|
||||
return;
|
||||
}
|
||||
for (auto& v : voices)
|
||||
if (v.active && v.note == midiNote)
|
||||
v.amp.noteOff();
|
||||
}
|
||||
|
||||
void SynthEngine::render (const VParams& p, float* left, float* right, int num)
|
||||
{
|
||||
float peakL = metL, peakR = metR;
|
||||
metL = metR = 0.0f;
|
||||
|
||||
for (auto& v : voices)
|
||||
{
|
||||
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
|
||||
v.flt.setRates (p.fA, p.fD, p.fS, p.fR, sr);
|
||||
}
|
||||
lastGlide = p.glide;
|
||||
lastPan = p.pan;
|
||||
polyLimit = juce::jlimit (1, maxVoice, (int) p.poly);
|
||||
|
||||
const float glideRate = p.glide <= 0.001f ? 1.0f
|
||||
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
|
||||
const float detuneRatio = std::pow (2.0f, p.detune / 1200.0f);
|
||||
const float tuneRatio = std::pow (2.0f, p.tune / 12.0f);
|
||||
const float maxFc = 18000.0f, minFc = 30.0f;
|
||||
|
||||
for (int s = 0; s < num; ++s)
|
||||
{
|
||||
float mixL = 0.0f, mixR = 0.0f;
|
||||
|
||||
for (auto& v : voices)
|
||||
{
|
||||
if (! v.active)
|
||||
continue;
|
||||
|
||||
if (p.glide > 0.001f)
|
||||
v.curFreq += (v.targetFreq - v.curFreq) * glideRate;
|
||||
else
|
||||
v.curFreq = v.targetFreq;
|
||||
|
||||
// LFO
|
||||
v.lfoPh += (double) p.lfoRate / sr;
|
||||
while (v.lfoPh >= 1.0)
|
||||
{
|
||||
v.lfoPh -= 1.0;
|
||||
v.lfoIdx++;
|
||||
v.lfoSH = rng.nextFloat() * 2.0f - 1.0f;
|
||||
}
|
||||
|
||||
float lfo = 0.0f;
|
||||
switch ((int) p.lfoShape)
|
||||
{
|
||||
case 0: lfo = (float) std::sin (v.lfoPh * 2.0 * juce::MathConstants<double>::pi); break;
|
||||
case 1: lfo = (float) (v.lfoPh < 0.5 ? 4.0 * v.lfoPh - 1.0 : 3.0 - 4.0 * v.lfoPh); break;
|
||||
case 2: lfo = v.lfoPh < 0.5 ? -1.0f : 1.0f; break;
|
||||
case 3: lfo = v.lfoSH; break;
|
||||
default: lfo = 0.0f; break;
|
||||
}
|
||||
|
||||
// frequency, vibrato, bend, tune
|
||||
float freq = v.curFreq * tuneRatio;
|
||||
if ((int) p.lfoTarget == 1)
|
||||
freq *= std::pow (2.0f, lfo * p.lfoDepth * 1.6f / 12.0f);
|
||||
if (bendSmooth != bendCents)
|
||||
bendSmooth += (bendCents - bendSmooth) * 0.08f;
|
||||
freq *= std::pow (2.0f, bendSmooth / 1200.0f);
|
||||
|
||||
const float d1 = (float) (freq / sr);
|
||||
const float d2 = d1 * detuneRatio;
|
||||
|
||||
v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0;
|
||||
v.ph2 += d2; if (v.ph2 >= 1.0) v.ph2 -= 1.0;
|
||||
|
||||
const float o1 = osc ((int) p.osc1w, v.ph1);
|
||||
const float o2 = osc ((int) p.osc2w, v.ph2);
|
||||
const float mixb = p.oscMix * 0.01f;
|
||||
float sig = o1 * (1.0f - mixb) + o2 * mixb;
|
||||
|
||||
const float fe = v.flt.next();
|
||||
|
||||
// cutoff with keytrack + filter envelope + LFO + mod wheel
|
||||
float fc = p.fltCut;
|
||||
fc *= std::pow (freq / 261.6256f, p.fltKey * 0.01f);
|
||||
fc *= std::pow (maxFc / fc, p.fltEnv * 0.01f * fe);
|
||||
if ((int) p.lfoTarget == 0)
|
||||
fc *= std::pow (2.0f, lfo * p.lfoDepth * 10.0f / 12.0f);
|
||||
fc *= std::pow (2.0f, mod);
|
||||
fc = std::min (maxFc, std::max (minFc, fc));
|
||||
|
||||
// SVF
|
||||
const float f1 = 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr);
|
||||
const float q1 = 1.0f / (0.5f + p.fltRes * 3.5f);
|
||||
v.f1c = f1;
|
||||
const float hp = sig - v.lp - q1 * v.bp;
|
||||
v.bp += f1 * hp;
|
||||
v.lp += f1 * v.bp;
|
||||
|
||||
float filtered = sig;
|
||||
switch ((int) p.fltType)
|
||||
{
|
||||
case 1: filtered = v.lp; break;
|
||||
case 2: filtered = v.bp; break;
|
||||
case 3: filtered = hp; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// amplitude envelope (+ tremolo if LFO targets amp)
|
||||
const float ae = v.amp.next();
|
||||
float ampMod = 1.0f;
|
||||
if ((int) p.lfoTarget == 2)
|
||||
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
|
||||
|
||||
v.outSmooth += (filtered - v.outSmooth) * 0.12f;
|
||||
const float o = v.outSmooth * (ae * ampMod) * v.vel;
|
||||
|
||||
mixL += o * v.panL;
|
||||
mixR += o * v.panR;
|
||||
|
||||
if (v.amp.getState() == Env::Off)
|
||||
{
|
||||
v.active = false;
|
||||
if (active > 0) --active;
|
||||
}
|
||||
}
|
||||
|
||||
peakL = std::max (peakL, std::abs (mixL));
|
||||
peakR = std::max (peakR, std::abs (mixR));
|
||||
|
||||
left[s] += mixL;
|
||||
right[s] += mixR;
|
||||
}
|
||||
|
||||
metL = peakL;
|
||||
metR = peakR;
|
||||
}
|
||||
144
Source/SynthEngine.h
Normal file
144
Source/SynthEngine.h
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "Params.h"
|
||||
|
||||
struct VParams
|
||||
{
|
||||
float osc1w = 2, osc2w = 3, detune = 8, oscMix = 52.f;
|
||||
float fltType = 1, fltCut = 3400.f, fltRes = 0.18f, fltEnv = 52.f, fltKey = 40.f;
|
||||
float ampA = 0.008f, ampD = 0.26f, ampS = 0.70f, ampR = 0.35f;
|
||||
float fA = 0.02f, fD = 0.16f, fS = 0.26f, fR = 0.25f;
|
||||
float lfoRate = 4.2f, lfoShape = 0, lfoDepth = 0.22f, lfoTarget = 0;
|
||||
float fxType = 1, fxAmt = 0.30f, fxTime = 0.28f;
|
||||
float vol = 0.85f, pan = 0.0f, poly = 10.f, glide = 0.f, tune = 0.f;
|
||||
|
||||
void update (const juce::AudioProcessorValueTreeState& apvts)
|
||||
{
|
||||
auto get = [&] (const char* id) { return apvts.getRawParameterValue (id) -> load(); };
|
||||
osc1w = get ("osc1wave"); osc2w = get ("osc2wave"); detune = get ("detune");
|
||||
oscMix = get ("oscmix");
|
||||
fltType = get ("flttype"); fltCut = get ("fltcut"); fltRes = get ("fltres");
|
||||
fltEnv = get ("fltenv"); fltKey = get ("fltkey");
|
||||
ampA = get ("ampA"); ampD = get ("ampD"); ampS = get ("ampS");
|
||||
ampR = get ("ampR");
|
||||
fA = get ("fenvA"); fD = get ("fenvD"); fS = get ("fenvS");
|
||||
fR = get ("fenvR");
|
||||
lfoRate = get ("lforate"); lfoShape = get ("lfoshape"); lfoDepth = get ("lfodepth");
|
||||
lfoTarget= get ("lfotarget");
|
||||
fxType = get ("fxtype"); fxAmt = get ("fxamt"); fxTime = get ("fxtime");
|
||||
vol = get ("vol"); pan = get ("pan"); poly = get ("poly");
|
||||
glide = get ("glide"); tune = get ("tune");
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Env
|
||||
{
|
||||
public:
|
||||
enum State { Off, Atk, Dec, Sus, Rel };
|
||||
|
||||
void setRates (float a, float d, float s, float r, double sr)
|
||||
{
|
||||
srInv = 1.0 / sr;
|
||||
attTau = 1.0f / (0.001f + 0.25f * a);
|
||||
decTau = 1.0f / (0.001f + 0.35f * d);
|
||||
relTau = 1.0f / (0.001f + 0.30f * r);
|
||||
susL = s;
|
||||
}
|
||||
|
||||
void noteOn () { st = Atk; }
|
||||
|
||||
void noteOff ()
|
||||
{
|
||||
if (st != Off && st != Rel) st = Rel;
|
||||
}
|
||||
|
||||
void reset (float v = 0.0f) { cur = v; st = Off; }
|
||||
|
||||
float next()
|
||||
{
|
||||
switch (st)
|
||||
{
|
||||
case Atk:
|
||||
cur += (1.0f - cur) * std::min (1.0f, (float) (attTau * srInv) * 8.0f);
|
||||
if (cur >= 1.0f) { cur = 1.0f; st = Dec; }
|
||||
break;
|
||||
case Dec:
|
||||
cur = susL + (cur - susL) * std::exp (-decTau * (float) srInv);
|
||||
if (std::abs (cur - susL) < 0.001f) { cur = susL; st = Sus; }
|
||||
break;
|
||||
case Sus: break;
|
||||
case Rel:
|
||||
cur *= std::exp (-relTau * (float) srInv);
|
||||
if (cur < 0.0002f) { cur = 0.0f; st = Off; }
|
||||
break;
|
||||
case Off: break;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
bool running() const { return st != Off; }
|
||||
float getCur() const { return cur; }
|
||||
State getState() const { return st; }
|
||||
|
||||
private:
|
||||
float cur = 0.0f, susL = 0.7f;
|
||||
float attTau = 1.0f, decTau = 1.0f, relTau = 1.0f;
|
||||
double srInv = 1.0 / 48000.0;
|
||||
State st = Off;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class SynthEngine
|
||||
{
|
||||
public:
|
||||
SynthEngine();
|
||||
~SynthEngine();
|
||||
|
||||
void setSampleRate (double sr);
|
||||
void noteOn (int midiNote, float velocity);
|
||||
void noteOff (int midiNote);
|
||||
void allNotesOff (bool immediate);
|
||||
void sustainPedal (bool down);
|
||||
void pitchBend (float cents);
|
||||
void modWheel (float v);
|
||||
|
||||
void render (const VParams& p, float* left, float* right, int numSamples);
|
||||
|
||||
int activeVoices() const noexcept { return active; }
|
||||
void getMeters (float& l, float& r) const noexcept { l = metL; r = metR; }
|
||||
|
||||
private:
|
||||
struct Voice
|
||||
{
|
||||
bool active = false;
|
||||
int note = 0;
|
||||
float vel = 1.0f;
|
||||
float panL = 0.71f, panR = 0.71f;
|
||||
float curFreq = 0.0f, targetFreq = 0.0f;
|
||||
double ph1 = 0.0, ph2 = 0.0, lfoPh = 0.0;
|
||||
float lfoSH = 0.0f;
|
||||
int lfoIdx = 0;
|
||||
Env amp, flt;
|
||||
float lp = 0.0f, bp = 0.0f, f1c = 0.0f;
|
||||
float outSmooth = 0.0f;
|
||||
};
|
||||
|
||||
static constexpr int maxVoice = 16;
|
||||
Voice voices[maxVoice];
|
||||
double sr = 48000.0;
|
||||
int active = 0;
|
||||
int polyLimit = 16;
|
||||
float mod = 0.0f, bendCents = 0.0f, bendSmooth = 0.0f;
|
||||
bool pedal = false;
|
||||
float lastNoteFreq = 440.0f, lastGlide = 0.0f, lastPan = 0.0f;
|
||||
int stealIdx = 0;
|
||||
float metL = 0.0f, metR = 0.0f;
|
||||
juce::Random rng;
|
||||
juce::Array<int> sustainedNotes;
|
||||
|
||||
float osc (int wave, double ph);
|
||||
Voice* acquireVoice();
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue